diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a64ab63494b35..61369a37ec3c2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -177,6 +177,8 @@ /x-pack/test/functional/services/ml/ @elastic/ml-ui /x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui /x-pack/test/functional_with_es_ssl/apps/ml/ @elastic/ml-ui +/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/ @elastic/ml-ui +/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/ @elastic/ml-ui # ML team owns and maintains the transform plugin despite it living in the Data management section. /x-pack/plugins/transform/ @elastic/ml-ui diff --git a/NOTICE.txt b/NOTICE.txt index 4ede43610ca7b..1694193892e16 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -295,7 +295,7 @@ MIT License http://www.opensource.org/licenses/mit-license --- This product includes code that is adapted from mapbox-gl-js, which is available under a "BSD-3-Clause" license. -https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js +https://github.com/mapbox/mapbox-gl-js/blob/v1.13.2/src/util/image.js Copyright (c) 2016, Mapbox diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 8f6f1f6c98ab2..63c29df44019d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -290,7 +290,14 @@ readonly links: { }>; readonly watcher: Record; readonly ccs: Record; - readonly plugins: Record; + readonly plugins: { + azureRepo: string; + gcsRepo: string; + hdfsRepo: string; + s3Repo: string; + snapshotRestoreRepos: string; + mapperSize: string; + }; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 77a250a14f929..27ea7f4dc7cd0 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -101,8 +101,8 @@ Changing these settings may disable features of the APM App. | `xpack.apm.indices.sourcemap` {ess-icon} | Matcher for all source map indices. Defaults to `apm-*`. -| `xpack.apm.autocreateApmIndexPattern` {ess-icon} - | Set to `false` to disable the automatic creation of the APM index pattern when the APM app is opened. Defaults to `true`. +| `xpack.apm.autoCreateApmDataView` {ess-icon} + | Set to `false` to disable the automatic creation of the APM data view when the APM app is opened. Defaults to `true`. |=== -// end::general-apm-settings[] \ No newline at end of file +// end::general-apm-settings[] diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index f0dfeb619bb38..a088f31937cc8 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -87,6 +87,7 @@ Optional properties are: `data_output_id`:: ID of the output to send data (Need to be identical to `monitoring_output_id`) `monitoring_output_id`:: ID of the output to send monitoring data. (Need to be identical to `data_output_id`) `package_policies`:: List of integration policies to add to this policy. + `id`::: Unique ID of the integration policy. The ID may be a number or string. `name`::: (required) Name of the integration policy. `package`::: (required) Integration that this policy configures `name`:::: Name of the integration associated with this policy. @@ -128,6 +129,7 @@ xpack.fleet.agentPolicies: - package: name: system name: System Integration + id: preconfigured-system inputs: - type: system/metrics enabled: true diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index d8bc26b7b3987..8bc98a028b8f6 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -72,6 +72,9 @@ For more information, see | `monitoring.ui.elasticsearch.ssl` | Shares the same configuration as <>. These settings configure encrypted communication between {kib} and the monitoring cluster. +| `monitoring.cluster_alerts.allowedSpaces` {ess-icon} + | Specifies the spaces where cluster Stack Monitoring alerts can be created. You must specify all spaces where you want to generate alerts, including the default space. Defaults to `[ "default" ]`. + |=== [float] diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index dd37943101145..3eb91a0d884ef 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -12,11 +12,3 @@ The maximum number of spaces that you can use with the {kib} instance. Some {kib return all spaces using a single `_search` from {es}, so you must configure this setting lower than the `index.max_result_window` in {es}. The default is `1000`. - -`monitoring.cluster_alerts.allowedSpaces` {ess-icon}:: -Specifies the spaces where cluster alerts are automatically generated. -You must specify all spaces where you want to generate alerts, including the default space. -When the default space is unspecified, {kib} is unable to generate an alert for the default space. -{es} clusters that run on {es} services are all containers. To send monitoring data -from your self-managed {es} installation to {es} services, set to `false`. -The default is `true`. diff --git a/package.json b/package.json index 6b7d6662eb70b..cc0ef06b39136 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,6 @@ "@elastic/ems-client": "8.0.0", "@elastic/eui": "41.0.0", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", "@elastic/react-search-ui": "^1.6.0", @@ -196,8 +195,10 @@ "archiver": "^5.2.0", "axios": "^0.21.1", "base64-js": "^1.3.1", + "bitmap-sdf": "^1.0.3", "brace": "0.11.1", "broadcast-channel": "^4.7.0", + "canvg": "^3.0.9", "chalk": "^4.1.0", "cheerio": "^1.0.0-rc.10", "chokidar": "^3.4.3", @@ -567,7 +568,9 @@ "@types/kbn__config": "link:bazel-bin/packages/kbn-config/npm_module_types", "@types/kbn__config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module_types", "@types/kbn__crypto": "link:bazel-bin/packages/kbn-crypto/npm_module_types", + "@types/kbn__dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module_types", "@types/kbn__docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module_types", + "@types/kbn__es-archiver": "link:bazel-bin/packages/kbn-es-archiver/npm_module_types", "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/license-checker": "15.0.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index aa90c3c122171..a7f0707575fcd 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -86,7 +86,9 @@ filegroup( "//packages/kbn-config:build_types", "//packages/kbn-config-schema:build_types", "//packages/kbn-crypto:build_types", + "//packages/kbn-dev-utils:build_types", "//packages/kbn-docs-utils:build_types", + "//packages/kbn-es-archiver:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", ], diff --git a/packages/elastic-eslint-config-kibana/react.js b/packages/elastic-eslint-config-kibana/react.js index 29000bdb15684..0b1cce15de9ad 100644 --- a/packages/elastic-eslint-config-kibana/react.js +++ b/packages/elastic-eslint-config-kibana/react.js @@ -1,5 +1,5 @@ const semver = require('semver'); -const { kibanaPackageJson: PKG } = require('@kbn/dev-utils'); +const { kibanaPackageJson: PKG } = require('@kbn/utils'); module.exports = { plugins: [ diff --git a/packages/elastic-eslint-config-kibana/typescript.js b/packages/elastic-eslint-config-kibana/typescript.js index 1a0ef81ae2f1e..3ada725cb1805 100644 --- a/packages/elastic-eslint-config-kibana/typescript.js +++ b/packages/elastic-eslint-config-kibana/typescript.js @@ -4,7 +4,7 @@ // as this package was moved from typescript-eslint-parser to @typescript-eslint/parser const semver = require('semver'); -const { kibanaPackageJson: PKG } = require('@kbn/dev-utils'); +const { kibanaPackageJson: PKG } = require('@kbn/utils'); const eslintConfigPrettierTypescriptEslintRules = require('eslint-config-prettier/@typescript-eslint').rules; diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index dfb441dffc6ef..cdc40e85c972a 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -50,7 +50,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-logging", "//packages/kbn-optimizer", "//packages/kbn-server-http-tools", diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts index e5e009e51e69e..0066644d0825a 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.test.ts @@ -8,11 +8,9 @@ import Path from 'path'; import * as Rx from 'rxjs'; -import { - REPO_ROOT, - createAbsolutePathSerializer, - createAnyInstanceSerializer, -} from '@kbn/dev-utils'; +import { createAbsolutePathSerializer, createAnyInstanceSerializer } from '@kbn/dev-utils'; + +import { REPO_ROOT } from '@kbn/utils'; import { TestLog } from './log'; import { CliDevMode, SomeCliArgs } from './cli_dev_mode'; diff --git a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts index 2396b316aa3a2..9cf688b675e67 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts @@ -22,7 +22,8 @@ import { takeUntil, } from 'rxjs/operators'; import { CliArgs } from '@kbn/config'; -import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils'; +import { CiStatsReporter } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Log, CliLog } from './log'; import { Optimizer } from './optimizer'; diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts index 06ded8d8bf526..25bc59bf78458 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getServerWatchPaths } from './get_server_watch_paths'; diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts index f075dc806b6ec..acfc9aeecdc80 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts @@ -9,7 +9,7 @@ import Path from 'path'; import Fs from 'fs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; interface Options { pluginPaths: string[]; diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 81ee6d770103c..f71c8b866fd5d 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -34,7 +34,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "@npm//@types/flot", "@npm//@types/jest", "@npm//@types/node", diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 4fd99e0144cb6..89df1870a3cec 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-dev-utils" PKG_REQUIRE_NAME = "@kbn/dev-utils" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__dev-utils" SOURCE_FILES = glob( [ @@ -43,7 +44,6 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ - "//packages/kbn-std", "//packages/kbn-utils", "@npm//@babel/core", "@npm//axios", @@ -66,7 +66,6 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-std", "//packages/kbn-utils", "@npm//@babel/parser", "@npm//@babel/types", @@ -124,7 +123,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -143,3 +142,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 9d6e6dde86fac..ab4f489e7d345 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -4,7 +4,6 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target_node/index.js", - "types": "./target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 381e99ac677f5..9b207ad9e9966 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export * from '@kbn/utils'; export { withProcRunner, ProcRunner } from './proc_runner'; export * from './tooling_log'; export * from './serializers'; diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel index 37e5bb06377cc..edfd3ee96c181 100644 --- a/packages/kbn-docs-utils/BUILD.bazel +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -38,7 +38,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-utils", "@npm//ts-morph", "@npm//@types/dedent", diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts index 2e4ce08540714..3c9137b260a3e 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts @@ -9,7 +9,8 @@ import Fs from 'fs'; import Path from 'path'; -import { REPO_ROOT, run, CiStatsReporter, createFlagError } from '@kbn/dev-utils'; +import { run, CiStatsReporter, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Project } from 'ts-morph'; import { writePluginDocs } from './mdx/write_plugin_mdx_docs'; @@ -241,7 +242,7 @@ export function runBuildApiDocsCli() { boolean: ['references'], help: ` --plugin Optionally, run for only a specific plugin - --stats Optionally print API stats. Must be one or more of: any, comments or exports. + --stats Optionally print API stats. Must be one or more of: any, comments or exports. --references Collect references for API items `, }, diff --git a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts index 78cba3f3a9476..774452a6f1f9f 100644 --- a/packages/kbn-docs-utils/src/api_docs/find_plugins.ts +++ b/packages/kbn-docs-utils/src/api_docs/find_plugins.ts @@ -12,7 +12,8 @@ import globby from 'globby'; import loadJsonFile from 'load-json-file'; import { getPluginSearchPaths } from '@kbn/config'; -import { simpleKibanaPlatformPluginDiscovery, REPO_ROOT } from '@kbn/dev-utils'; +import { simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ApiScope, PluginOrPackage } from './types'; export function findPlugins(): PluginOrPackage[] { diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index 2dc311ed74406..da8aaf913ab67 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-es-archiver" PKG_REQUIRE_NAME = "@kbn/es-archiver" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__es-archiver" SOURCE_FILES = glob( [ @@ -43,7 +44,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-test", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", @@ -90,7 +91,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -109,3 +110,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 0cce08eaf0352..bff3990a0c1bc 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -4,7 +4,6 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "private": "true", "main": "target_node/index.js", - "types": "target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index 0a7235c566b52..c5bea5e29a687 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -9,7 +9,8 @@ import { resolve, relative } from 'path'; import { createReadStream } from 'fs'; import { Readable } from 'stream'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { KbnClient } from '@kbn/test'; import type { Client } from '@elastic/elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '@kbn/utils'; diff --git a/packages/kbn-es-archiver/src/actions/rebuild_all.ts b/packages/kbn-es-archiver/src/actions/rebuild_all.ts index 360fdb438f2db..27fcae0c7cec5 100644 --- a/packages/kbn-es-archiver/src/actions/rebuild_all.ts +++ b/packages/kbn-es-archiver/src/actions/rebuild_all.ts @@ -10,8 +10,8 @@ import { resolve, relative } from 'path'; import { Stats, createReadStream, createWriteStream } from 'fs'; import { stat, rename } from 'fs/promises'; import { Readable, Writable } from 'stream'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { createPromiseFromStreams } from '@kbn/utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { createPromiseFromStreams, REPO_ROOT } from '@kbn/utils'; import { prioritizeMappings, readDirectory, diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 9cb5be05ac060..e5e3f06b8436d 100644 --- a/packages/kbn-es-archiver/src/actions/save.ts +++ b/packages/kbn-es-archiver/src/actions/save.ts @@ -10,8 +10,8 @@ import { resolve, relative } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; import { Readable, Writable } from 'stream'; import type { Client } from '@elastic/elasticsearch'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { createListStream, createPromiseFromStreams } from '@kbn/utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { createListStream, createPromiseFromStreams, REPO_ROOT } from '@kbn/utils'; import { createStats, diff --git a/packages/kbn-es-archiver/src/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts index 1c5f4cd5d7d03..22830b7289174 100644 --- a/packages/kbn-es-archiver/src/actions/unload.ts +++ b/packages/kbn-es-archiver/src/actions/unload.ts @@ -10,9 +10,9 @@ import { resolve, relative } from 'path'; import { createReadStream } from 'fs'; import { Readable, Writable } from 'stream'; import type { Client } from '@elastic/elasticsearch'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { createPromiseFromStreams } from '@kbn/utils'; +import { createPromiseFromStreams, REPO_ROOT } from '@kbn/utils'; import { isGzip, diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index 354197a98fa46..e13e20f25a703 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -10,7 +10,8 @@ import Fs from 'fs'; import Path from 'path'; import type { Client } from '@elastic/elasticsearch'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { KbnClient } from '@kbn/test'; import { diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts index ae21649690a99..2590074a25411 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ +import { ToolingLog } from '@kbn/dev-utils'; + import { createListStream, createPromiseFromStreams, createConcatStream, createMapStream, - ToolingLog, -} from '@kbn/dev-utils'; +} from '@kbn/utils'; import { createGenerateDocRecordsStream } from './generate_doc_records_stream'; import { Progress } from '../progress'; diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index bcf28a4976a1c..9c0ff4a8f91ec 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -6,12 +6,9 @@ * Side Public License, v 1. */ -import { - createListStream, - createPromiseFromStreams, - ToolingLog, - createRecursiveSerializer, -} from '@kbn/dev-utils'; +import { ToolingLog, createRecursiveSerializer } from '@kbn/dev-utils'; + +import { createListStream, createPromiseFromStreams } from '@kbn/utils'; import { Progress } from '../progress'; import { createIndexDocRecordsStream } from './index_doc_records_stream'; diff --git a/packages/kbn-es/BUILD.bazel b/packages/kbn-es/BUILD.bazel index 91dc48cec7d0c..2ea9c32858dd3 100644 --- a/packages/kbn-es/BUILD.bazel +++ b/packages/kbn-es/BUILD.bazel @@ -1,5 +1,5 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm") PKG_BASE_NAME = "kbn-es" PKG_REQUIRE_NAME = "@kbn/es" diff --git a/packages/kbn-es/src/artifact.test.js b/packages/kbn-es/src/artifact.test.js index c65109bc34610..884804ed75a65 100644 --- a/packages/kbn-es/src/artifact.test.js +++ b/packages/kbn-es/src/artifact.test.js @@ -90,10 +90,10 @@ const artifactTest = (requestedLicense, expectedLicense, fetchTimesCalled = 1) = `${PERMANENT_SNAPSHOT_BASE_URL}/${MOCK_VERSION}/manifest.json` ); } - expect(artifact.getUrl()).toEqual(MOCK_URL + `/${expectedLicense}`); - expect(artifact.getChecksumUrl()).toEqual(MOCK_URL + `/${expectedLicense}.sha512`); - expect(artifact.getChecksumType()).toEqual('sha512'); - expect(artifact.getFilename()).toEqual(MOCK_FILENAME + `-${ARCHITECTURE}.${expectedLicense}`); + expect(artifact.spec.url).toEqual(MOCK_URL + `/${expectedLicense}`); + expect(artifact.spec.checksumUrl).toEqual(MOCK_URL + `/${expectedLicense}.sha512`); + expect(artifact.spec.checksumType).toEqual('sha512'); + expect(artifact.spec.filename).toEqual(MOCK_FILENAME + `-${ARCHITECTURE}.${expectedLicense}`); }; }; @@ -158,7 +158,7 @@ describe('Artifact', () => { it('should return artifact metadata for the correct architecture', async () => { const artifact = await Artifact.getSnapshot('oss', MOCK_VERSION, log); - expect(artifact.getFilename()).toEqual(MOCK_FILENAME + `-${ARCHITECTURE}.oss`); + expect(artifact.spec.filename).toEqual(MOCK_FILENAME + `-${ARCHITECTURE}.oss`); }); }); @@ -182,7 +182,7 @@ describe('Artifact', () => { describe('with latest unverified snapshot', () => { beforeEach(() => { - process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 1; + process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = '1'; mockFetch(MOCKS.valid); }); diff --git a/packages/kbn-es/src/artifact.js b/packages/kbn-es/src/artifact.ts similarity index 65% rename from packages/kbn-es/src/artifact.js rename to packages/kbn-es/src/artifact.ts index 0fa2c7a1727d0..9c5935c96e8cd 100644 --- a/packages/kbn-es/src/artifact.js +++ b/packages/kbn-es/src/artifact.ts @@ -6,25 +6,69 @@ * Side Public License, v 1. */ -const fetch = require('node-fetch'); -const AbortController = require('abort-controller'); -const fs = require('fs'); -const { promisify } = require('util'); -const { pipeline, Transform } = require('stream'); -const chalk = require('chalk'); -const { createHash } = require('crypto'); -const path = require('path'); +import fs from 'fs'; +import { promisify } from 'util'; +import path from 'path'; +import { createHash } from 'crypto'; +import { pipeline, Transform } from 'stream'; +import { setTimeout } from 'timers/promises'; + +import fetch, { Headers } from 'node-fetch'; +import AbortController from 'abort-controller'; +import chalk from 'chalk'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { cache } from './utils/cache'; +import { resolveCustomSnapshotUrl } from './custom_snapshots'; +import { createCliError, isCliError } from './errors'; const asyncPipeline = promisify(pipeline); const DAILY_SNAPSHOTS_BASE_URL = 'https://storage.googleapis.com/kibana-ci-es-snapshots-daily'; const PERMANENT_SNAPSHOTS_BASE_URL = 'https://storage.googleapis.com/kibana-ci-es-snapshots-permanent'; -const { cache } = require('./utils'); -const { resolveCustomSnapshotUrl } = require('./custom_snapshots'); -const { createCliError, isCliError } = require('./errors'); +type ChecksumType = 'sha512'; +export type ArtifactLicense = 'oss' | 'basic' | 'trial'; + +interface ArtifactManifest { + id: string; + bucket: string; + branch: string; + sha: string; + sha_short: string; + version: string; + generated: string; + archives: Array<{ + filename: string; + checksum: string; + url: string; + version: string; + platform: string; + architecture: string; + license: string; + }>; +} + +export interface ArtifactSpec { + url: string; + checksumUrl: string; + checksumType: ChecksumType; + filename: string; +} + +interface ArtifactDownloaded { + cached: false; + checksum: string; + etag?: string; + contentLength: number; + first500Bytes: Buffer; + headers: Headers; +} +interface ArtifactCached { + cached: true; +} -function getChecksumType(checksumUrl) { +function getChecksumType(checksumUrl: string): ChecksumType { if (checksumUrl.endsWith('.sha512')) { return 'sha512'; } @@ -32,15 +76,18 @@ function getChecksumType(checksumUrl) { throw new Error(`unable to determine checksum type: ${checksumUrl}`); } -function headersToString(headers, indent = '') { +function headersToString(headers: Headers, indent = '') { return [...headers.entries()].reduce( (acc, [key, value]) => `${acc}\n${indent}${key}: ${value}`, '' ); } -async function retry(log, fn) { - async function doAttempt(attempt) { +async function retry(log: ToolingLog, fn: () => Promise): Promise { + let attempt = 0; + while (true) { + attempt += 1; + try { return await fn(); } catch (error) { @@ -49,13 +96,10 @@ async function retry(log, fn) { } log.warning('...failure, retrying in 5 seconds:', error.message); - await new Promise((resolve) => setTimeout(resolve, 5000)); + await setTimeout(5000); log.info('...retrying'); - return await doAttempt(attempt + 1); } } - - return await doAttempt(1); } // Setting this flag provides an easy way to run the latest un-promoted snapshot without having to look it up @@ -63,7 +107,7 @@ function shouldUseUnverifiedSnapshot() { return !!process.env.KBN_ES_SNAPSHOT_USE_UNVERIFIED; } -async function fetchSnapshotManifest(url, log) { +async function fetchSnapshotManifest(url: string, log: ToolingLog) { log.info('Downloading snapshot manifest from %s', chalk.bold(url)); const abc = new AbortController(); @@ -73,7 +117,11 @@ async function fetchSnapshotManifest(url, log) { return { abc, resp, json }; } -async function getArtifactSpecForSnapshot(urlVersion, license, log) { +async function getArtifactSpecForSnapshot( + urlVersion: string, + license: string, + log: ToolingLog +): Promise { const desiredVersion = urlVersion.replace('-SNAPSHOT', ''); const desiredLicense = license === 'oss' ? 'oss' : 'default'; @@ -103,17 +151,16 @@ async function getArtifactSpecForSnapshot(urlVersion, license, log) { throw new Error(`Unable to read snapshot manifest: ${resp.statusText}\n ${json}`); } - const manifest = JSON.parse(json); - + const manifest: ArtifactManifest = JSON.parse(json); const platform = process.platform === 'win32' ? 'windows' : process.platform; const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64'; const archive = manifest.archives.find( - (archive) => - archive.version === desiredVersion && - archive.platform === platform && - archive.license === desiredLicense && - archive.architecture === arch + (a) => + a.version === desiredVersion && + a.platform === platform && + a.license === desiredLicense && + a.architecture === arch ); if (!archive) { @@ -130,93 +177,65 @@ async function getArtifactSpecForSnapshot(urlVersion, license, log) { }; } -exports.Artifact = class Artifact { +export class Artifact { /** * Fetch an Artifact from the Artifact API for a license level and version - * @param {('oss'|'basic'|'trial')} license - * @param {string} version - * @param {ToolingLog} log */ - static async getSnapshot(license, version, log) { + static async getSnapshot(license: ArtifactLicense, version: string, log: ToolingLog) { const urlVersion = `${encodeURIComponent(version)}-SNAPSHOT`; const customSnapshotArtifactSpec = resolveCustomSnapshotUrl(urlVersion, license); if (customSnapshotArtifactSpec) { - return new Artifact(customSnapshotArtifactSpec, log); + return new Artifact(log, customSnapshotArtifactSpec); } const artifactSpec = await getArtifactSpecForSnapshot(urlVersion, license, log); - return new Artifact(artifactSpec, log); + return new Artifact(log, artifactSpec); } /** * Fetch an Artifact from the Elasticsearch past releases url - * @param {string} url - * @param {ToolingLog} log */ - static async getArchive(url, log) { + static async getArchive(url: string, log: ToolingLog) { const shaUrl = `${url}.sha512`; - const artifactSpec = { - url: url, + return new Artifact(log, { + url, filename: path.basename(url), checksumUrl: shaUrl, checksumType: getChecksumType(shaUrl), - }; - - return new Artifact(artifactSpec, log); - } - - constructor(spec, log) { - this._spec = spec; - this._log = log; - } - - getUrl() { - return this._spec.url; - } - - getChecksumUrl() { - return this._spec.checksumUrl; + }); } - getChecksumType() { - return this._spec.checksumType; - } - - getFilename() { - return this._spec.filename; - } + constructor(private readonly log: ToolingLog, public readonly spec: ArtifactSpec) {} /** * Download the artifact to disk, skips the download if the cache is * up-to-date, verifies checksum when downloaded - * @param {string} dest - * @return {Promise} */ - async download(dest, { useCached = false }) { - await retry(this._log, async () => { + async download(dest: string, { useCached = false }: { useCached?: boolean } = {}) { + await retry(this.log, async () => { const cacheMeta = cache.readMeta(dest); const tmpPath = `${dest}.tmp`; if (useCached) { if (cacheMeta.exists) { - this._log.info( + this.log.info( 'use-cached passed, forcing to use existing snapshot', chalk.bold(cacheMeta.ts) ); return; } else { - this._log.info('use-cached passed but no cached snapshot found. Continuing to download'); + this.log.info('use-cached passed but no cached snapshot found. Continuing to download'); } } - const artifactResp = await this._download(tmpPath, cacheMeta.etag, cacheMeta.ts); + const artifactResp = await this.fetchArtifact(tmpPath, cacheMeta.etag, cacheMeta.ts); if (artifactResp.cached) { return; } - await this._verifyChecksum(artifactResp); + await this.verifyChecksum(artifactResp); // cache the etag for future downloads cache.writeMeta(dest, { etag: artifactResp.etag }); @@ -228,18 +247,18 @@ exports.Artifact = class Artifact { /** * Fetch the artifact with an etag - * @param {string} tmpPath - * @param {string} etag - * @param {string} ts - * @return {{ cached: true }|{ checksum: string, etag: string, first500Bytes: Buffer }} */ - async _download(tmpPath, etag, ts) { - const url = this.getUrl(); + private async fetchArtifact( + tmpPath: string, + etag: string, + ts: string + ): Promise { + const url = this.spec.url; if (etag) { - this._log.info('verifying cache of %s', chalk.bold(url)); + this.log.info('verifying cache of %s', chalk.bold(url)); } else { - this._log.info('downloading artifact from %s', chalk.bold(url)); + this.log.info('downloading artifact from %s', chalk.bold(url)); } const abc = new AbortController(); @@ -251,7 +270,7 @@ exports.Artifact = class Artifact { }); if (resp.status === 304) { - this._log.info('etags match, reusing cache from %s', chalk.bold(ts)); + this.log.info('etags match, reusing cache from %s', chalk.bold(ts)); abc.abort(); return { @@ -270,10 +289,10 @@ exports.Artifact = class Artifact { } if (etag) { - this._log.info('cache invalid, redownloading'); + this.log.info('cache invalid, redownloading'); } - const hash = createHash(this.getChecksumType()); + const hash = createHash(this.spec.checksumType); let first500Bytes = Buffer.alloc(0); let contentLength = 0; @@ -300,8 +319,9 @@ exports.Artifact = class Artifact { ); return { + cached: false, checksum: hash.digest('hex'), - etag: resp.headers.get('etag'), + etag: resp.headers.get('etag') ?? undefined, contentLength, first500Bytes, headers: resp.headers, @@ -310,14 +330,12 @@ exports.Artifact = class Artifact { /** * Verify the checksum of the downloaded artifact with the checksum at checksumUrl - * @param {{ checksum: string, contentLength: number, first500Bytes: Buffer }} artifactResp - * @return {Promise} */ - async _verifyChecksum(artifactResp) { - this._log.info('downloading artifact checksum from %s', chalk.bold(this.getChecksumUrl())); + private async verifyChecksum(artifactResp: ArtifactDownloaded) { + this.log.info('downloading artifact checksum from %s', chalk.bold(this.spec.checksumUrl)); const abc = new AbortController(); - const resp = await fetch(this.getChecksumUrl(), { + const resp = await fetch(this.spec.checksumUrl, { signal: abc.signal, }); @@ -338,7 +356,7 @@ exports.Artifact = class Artifact { const lenString = `${len} / ${artifactResp.contentLength}`; throw createCliError( - `artifact downloaded from ${this.getUrl()} does not match expected checksum\n` + + `artifact downloaded from ${this.spec.url} does not match expected checksum\n` + ` expected: ${expectedChecksum}\n` + ` received: ${artifactResp.checksum}\n` + ` headers: ${headersToString(artifactResp.headers, ' ')}\n` + @@ -346,6 +364,6 @@ exports.Artifact = class Artifact { ); } - this._log.info('checksum verified'); + this.log.info('checksum verified'); } -}; +} diff --git a/packages/kbn-es/src/custom_snapshots.js b/packages/kbn-es/src/custom_snapshots.ts similarity index 82% rename from packages/kbn-es/src/custom_snapshots.js rename to packages/kbn-es/src/custom_snapshots.ts index 9dd8097244947..f3e6d3ecaf857 100644 --- a/packages/kbn-es/src/custom_snapshots.js +++ b/packages/kbn-es/src/custom_snapshots.ts @@ -6,13 +6,15 @@ * Side Public License, v 1. */ -const { basename } = require('path'); +import Path from 'path'; -function isVersionFlag(a) { +import type { ArtifactSpec } from './artifact'; + +function isVersionFlag(a: string) { return a.startsWith('--version'); } -function getCustomSnapshotUrl() { +export function getCustomSnapshotUrl() { // force use of manually created snapshots until ReindexPutMappings fix if ( !process.env.ES_SNAPSHOT_MANIFEST && @@ -28,7 +30,10 @@ function getCustomSnapshotUrl() { } } -function resolveCustomSnapshotUrl(urlVersion, license) { +export function resolveCustomSnapshotUrl( + urlVersion: string, + license: string +): ArtifactSpec | undefined { const customSnapshotUrl = getCustomSnapshotUrl(); if (!customSnapshotUrl) { @@ -48,8 +53,6 @@ function resolveCustomSnapshotUrl(urlVersion, license) { url: overrideUrl, checksumUrl: overrideUrl + '.sha512', checksumType: 'sha512', - filename: basename(overrideUrl), + filename: Path.basename(overrideUrl), }; } - -module.exports = { getCustomSnapshotUrl, resolveCustomSnapshotUrl }; diff --git a/packages/kbn-es/src/errors.ts b/packages/kbn-es/src/errors.ts new file mode 100644 index 0000000000000..a0c526dc48a9c --- /dev/null +++ b/packages/kbn-es/src/errors.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +interface CliError extends Error { + isCliError: boolean; +} + +export function createCliError(message: string) { + return Object.assign(new Error(message), { + isCliError: true, + }); +} + +function isObj(x: unknown): x is Record { + return typeof x === 'object' && x !== null; +} + +export function isCliError(error: unknown): error is CliError { + return isObj(error) && error.isCliError === true; +} diff --git a/packages/kbn-es/src/index.js b/packages/kbn-es/src/index.ts similarity index 72% rename from packages/kbn-es/src/index.js rename to packages/kbn-es/src/index.ts index 3b12de68234fa..68fd931794c0c 100644 --- a/packages/kbn-es/src/index.js +++ b/packages/kbn-es/src/index.ts @@ -6,5 +6,7 @@ * Side Public License, v 1. */ -exports.run = require('./cli').run; -exports.Cluster = require('./cluster').Cluster; +// @ts-expect-error not typed yet +export { run } from './cli'; +// @ts-expect-error not typed yet +export { Cluster } from './cluster'; diff --git a/packages/kbn-es/src/install/index.js b/packages/kbn-es/src/install/index.ts similarity index 58% rename from packages/kbn-es/src/install/index.js rename to packages/kbn-es/src/install/index.ts index 07582f73c663a..e827dee2247f9 100644 --- a/packages/kbn-es/src/install/index.js +++ b/packages/kbn-es/src/install/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -exports.installArchive = require('./archive').installArchive; -exports.installSnapshot = require('./snapshot').installSnapshot; -exports.downloadSnapshot = require('./snapshot').downloadSnapshot; -exports.installSource = require('./source').installSource; +export { installArchive } from './install_archive'; +export { installSnapshot, downloadSnapshot } from './install_snapshot'; +export { installSource } from './install_source'; diff --git a/packages/kbn-es/src/install/archive.js b/packages/kbn-es/src/install/install_archive.ts similarity index 64% rename from packages/kbn-es/src/install/archive.js rename to packages/kbn-es/src/install/install_archive.ts index 76db5a4427e6d..ee04d9e4b62b5 100644 --- a/packages/kbn-es/src/install/archive.js +++ b/packages/kbn-es/src/install/install_archive.ts @@ -6,29 +6,40 @@ * Side Public License, v 1. */ -const fs = require('fs'); -const path = require('path'); -const chalk = require('chalk'); -const execa = require('execa'); -const del = require('del'); -const url = require('url'); -const { extract } = require('@kbn/dev-utils'); -const { log: defaultLog } = require('../utils'); -const { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } = require('../paths'); -const { Artifact } = require('../artifact'); -const { parseSettings, SettingsFilter } = require('../settings'); +import fs from 'fs'; +import path from 'path'; + +import chalk from 'chalk'; +import execa from 'execa'; +import del from 'del'; +import { extract, ToolingLog } from '@kbn/dev-utils'; + +import { BASE_PATH, ES_CONFIG, ES_KEYSTORE_BIN } from '../paths'; +import { Artifact } from '../artifact'; +import { parseSettings, SettingsFilter } from '../settings'; +import { log as defaultLog } from '../utils/log'; + +interface InstallArchiveOptions { + license?: string; + password?: string; + basePath?: string; + installPath?: string; + log?: ToolingLog; + esArgs?: string[]; +} + +const isHttpUrl = (str: string) => { + try { + return ['http:', 'https:'].includes(new URL(str).protocol); + } catch { + return false; + } +}; /** * Extracts an ES archive and optionally installs plugins - * - * @param {String} archive - path to tar - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.installArchive = async function installArchive(archive, options = {}) { +export async function installArchive(archive: string, options: InstallArchiveOptions = {}) { const { license = 'basic', password = 'changeme', @@ -39,9 +50,9 @@ exports.installArchive = async function installArchive(archive, options = {}) { } = options; let dest = archive; - if (['http:', 'https:'].includes(url.parse(archive).protocol)) { + if (isHttpUrl(archive)) { const artifact = await Artifact.getArchive(archive, log); - dest = path.resolve(basePath, 'cache', artifact.getFilename()); + dest = path.resolve(basePath, 'cache', artifact.spec.filename); await artifact.download(dest); } @@ -75,28 +86,23 @@ exports.installArchive = async function installArchive(archive, options = {}) { } return { installPath }; -}; +} /** * Appends single line to elasticsearch.yml config file - * - * @param {String} installPath - * @param {String} key - * @param {String} value */ -async function appendToConfig(installPath, key, value) { +async function appendToConfig(installPath: string, key: string, value: string) { fs.appendFileSync(path.resolve(installPath, ES_CONFIG), `${key}: ${value}\n`, 'utf8'); } /** * Creates and configures Keystore - * - * @param {String} installPath - * @param {ToolingLog} log - * @param {Array<[string, string]>} secureSettings List of custom Elasticsearch secure settings to - * add into the keystore. */ -async function configureKeystore(installPath, log = defaultLog, secureSettings) { +async function configureKeystore( + installPath: string, + log: ToolingLog = defaultLog, + secureSettings: Array<[string, string]> +) { const env = { JAVA_HOME: '' }; await execa(ES_KEYSTORE_BIN, ['create'], { cwd: installPath, env }); diff --git a/packages/kbn-es/src/install/snapshot.js b/packages/kbn-es/src/install/install_snapshot.ts similarity index 55% rename from packages/kbn-es/src/install/snapshot.js rename to packages/kbn-es/src/install/install_snapshot.ts index cf1ce50f7e413..84d713745eb82 100644 --- a/packages/kbn-es/src/install/snapshot.js +++ b/packages/kbn-es/src/install/install_snapshot.ts @@ -6,56 +6,58 @@ * Side Public License, v 1. */ -const chalk = require('chalk'); -const path = require('path'); -const { BASE_PATH } = require('../paths'); -const { installArchive } = require('./archive'); -const { log: defaultLog } = require('../utils'); -const { Artifact } = require('../artifact'); +import path from 'path'; + +import chalk from 'chalk'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { BASE_PATH } from '../paths'; +import { installArchive } from './install_archive'; +import { log as defaultLog } from '../utils/log'; +import { Artifact, ArtifactLicense } from '../artifact'; + +interface DownloadSnapshotOptions { + version: string; + license?: ArtifactLicense; + basePath?: string; + installPath?: string; + log?: ToolingLog; + useCached?: boolean; +} /** * Download an ES snapshot - * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.version - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.downloadSnapshot = async function installSnapshot({ +export async function downloadSnapshot({ license = 'basic', version, basePath = BASE_PATH, installPath = path.resolve(basePath, version), log = defaultLog, useCached = false, -}) { +}: DownloadSnapshotOptions) { log.info('version: %s', chalk.bold(version)); log.info('install path: %s', chalk.bold(installPath)); log.info('license: %s', chalk.bold(license)); const artifact = await Artifact.getSnapshot(license, version, log); - const dest = path.resolve(basePath, 'cache', artifact.getFilename()); + const dest = path.resolve(basePath, 'cache', artifact.spec.filename); await artifact.download(dest, { useCached }); return { downloadPath: dest, }; -}; +} + +interface InstallSnapshotOptions extends DownloadSnapshotOptions { + password?: string; + esArgs?: string[]; +} /** * Installs ES from snapshot - * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.password - * @property {String} options.version - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.installSnapshot = async function installSnapshot({ +export async function installSnapshot({ license = 'basic', password = 'password', version, @@ -64,8 +66,8 @@ exports.installSnapshot = async function installSnapshot({ log = defaultLog, esArgs, useCached = false, -}) { - const { downloadPath } = await exports.downloadSnapshot({ +}: InstallSnapshotOptions) { + const { downloadPath } = await downloadSnapshot({ license, version, basePath, @@ -82,4 +84,4 @@ exports.installSnapshot = async function installSnapshot({ log, esArgs, }); -}; +} diff --git a/packages/kbn-es/src/install/source.js b/packages/kbn-es/src/install/install_source.ts similarity index 73% rename from packages/kbn-es/src/install/source.js rename to packages/kbn-es/src/install/install_source.ts index 81a1019509906..d8c272677058e 100644 --- a/packages/kbn-es/src/install/source.js +++ b/packages/kbn-es/src/install/install_source.ts @@ -6,28 +6,35 @@ * Side Public License, v 1. */ -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const chalk = require('chalk'); -const crypto = require('crypto'); -const simpleGit = require('simple-git/promise'); -const { installArchive } = require('./archive'); -const { log: defaultLog, cache, buildSnapshot, archiveForPlatform } = require('../utils'); -const { BASE_PATH } = require('../paths'); +import path from 'path'; +import fs from 'fs'; +import os from 'os'; +import crypto from 'crypto'; + +import chalk from 'chalk'; +import simpleGit from 'simple-git/promise'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { installArchive } from './install_archive'; +import { log as defaultLog } from '../utils/log'; +import { cache } from '../utils/cache'; +import { buildSnapshot, archiveForPlatform } from '../utils/build_snapshot'; +import { BASE_PATH } from '../paths'; + +interface InstallSourceOptions { + sourcePath: string; + license?: string; + password?: string; + basePath?: string; + installPath?: string; + log?: ToolingLog; + esArgs?: string[]; +} /** * Installs ES from source - * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.password - * @property {String} options.sourcePath - * @property {String} options.basePath - * @property {String} options.installPath - * @property {ToolingLog} options.log */ -exports.installSource = async function installSource({ +export async function installSource({ license = 'basic', password = 'changeme', sourcePath, @@ -35,7 +42,7 @@ exports.installSource = async function installSource({ installPath = path.resolve(basePath, 'source'), log = defaultLog, esArgs, -}) { +}: InstallSourceOptions) { log.info('source path: %s', chalk.bold(sourcePath)); log.info('install path: %s', chalk.bold(installPath)); log.info('license: %s', chalk.bold(license)); @@ -62,14 +69,9 @@ exports.installSource = async function installSource({ log, esArgs, }); -}; +} -/** - * - * @param {String} cwd - * @param {ToolingLog} log - */ -async function sourceInfo(cwd, license, log = defaultLog) { +async function sourceInfo(cwd: string, license: string, log: ToolingLog = defaultLog) { if (!fs.existsSync(cwd)) { throw new Error(`${cwd} does not exist`); } diff --git a/packages/kbn-es/src/paths.js b/packages/kbn-es/src/paths.js deleted file mode 100644 index 5c8d3b654ecf9..0000000000000 --- a/packages/kbn-es/src/paths.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const os = require('os'); -const path = require('path'); - -function maybeUseBat(bin) { - return os.platform().startsWith('win') ? `${bin}.bat` : bin; -} - -const tempDir = os.tmpdir(); - -exports.BASE_PATH = path.resolve(tempDir, 'kbn-es'); - -exports.GRADLE_BIN = maybeUseBat('./gradlew'); -exports.ES_BIN = maybeUseBat('bin/elasticsearch'); -exports.ES_CONFIG = 'config/elasticsearch.yml'; - -exports.ES_KEYSTORE_BIN = maybeUseBat('./bin/elasticsearch-keystore'); diff --git a/packages/kbn-es/src/paths.ts b/packages/kbn-es/src/paths.ts new file mode 100644 index 0000000000000..c1b859af4e1f5 --- /dev/null +++ b/packages/kbn-es/src/paths.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Os from 'os'; +import Path from 'path'; + +function maybeUseBat(bin: string) { + return Os.platform().startsWith('win') ? `${bin}.bat` : bin; +} + +const tempDir = Os.tmpdir(); + +export const BASE_PATH = Path.resolve(tempDir, 'kbn-es'); + +export const GRADLE_BIN = maybeUseBat('./gradlew'); +export const ES_BIN = maybeUseBat('bin/elasticsearch'); +export const ES_CONFIG = 'config/elasticsearch.yml'; + +export const ES_KEYSTORE_BIN = maybeUseBat('./bin/elasticsearch-keystore'); diff --git a/packages/kbn-es/src/utils/build_snapshot.js b/packages/kbn-es/src/utils/build_snapshot.ts similarity index 53% rename from packages/kbn-es/src/utils/build_snapshot.js rename to packages/kbn-es/src/utils/build_snapshot.ts index ec26ba69e658b..542e63dcc0748 100644 --- a/packages/kbn-es/src/utils/build_snapshot.js +++ b/packages/kbn-es/src/utils/build_snapshot.ts @@ -6,25 +6,25 @@ * Side Public License, v 1. */ -const execa = require('execa'); -const path = require('path'); -const os = require('os'); -const readline = require('readline'); -const { createCliError } = require('../errors'); -const { findMostRecentlyChanged } = require('../utils'); -const { GRADLE_BIN } = require('../paths'); +import path from 'path'; +import os from 'os'; -const onceEvent = (emitter, event) => new Promise((resolve) => emitter.once(event, resolve)); +import { ToolingLog, withProcRunner } from '@kbn/dev-utils'; + +import { createCliError } from '../errors'; +import { findMostRecentlyChanged } from './find_most_recently_changed'; +import { GRADLE_BIN } from '../paths'; + +interface BuildSnapshotOptions { + license: string; + sourcePath: string; + log: ToolingLog; + platform?: string; +} /** * Creates archive from source * - * @param {Object} options - * @property {('oss'|'basic'|'trial')} options.license - * @property {String} options.sourcePath - * @property {ToolingLog} options.log - * @returns {Object} containing archive and optional plugins - * * Gradle tasks: * $ ./gradlew tasks --all | grep 'distribution.*assemble\s' * :distribution:archives:darwin-tar:assemble @@ -34,39 +34,27 @@ const onceEvent = (emitter, event) => new Promise((resolve) => emitter.once(even * :distribution:archives:oss-linux-tar:assemble * :distribution:archives:oss-windows-zip:assemble */ -exports.buildSnapshot = async ({ license, sourcePath, log, platform = os.platform() }) => { +export async function buildSnapshot({ + license, + sourcePath, + log, + platform = os.platform(), +}: BuildSnapshotOptions) { const { task, ext } = exports.archiveForPlatform(platform, license); const buildArgs = [`:distribution:archives:${task}:assemble`]; log.info('%s %s', GRADLE_BIN, buildArgs.join(' ')); log.debug('cwd:', sourcePath); - const build = execa(GRADLE_BIN, buildArgs, { - cwd: sourcePath, - stdio: ['ignore', 'pipe', 'pipe'], + await withProcRunner(log, async (procs) => { + await procs.run('gradle', { + cmd: GRADLE_BIN, + args: buildArgs, + cwd: sourcePath, + wait: true, + }); }); - const stdout = readline.createInterface({ input: build.stdout }); - const stderr = readline.createInterface({ input: build.stderr }); - - stdout.on('line', (line) => log.debug(line)); - stderr.on('line', (line) => log.error(line)); - - const [exitCode] = await Promise.all([ - Promise.race([ - onceEvent(build, 'exit'), - onceEvent(build, 'error').then((error) => { - throw createCliError(`Error spawning gradle: ${error.message}`); - }), - ]), - onceEvent(stdout, 'close'), - onceEvent(stderr, 'close'), - ]); - - if (exitCode > 0) { - throw createCliError('unable to build ES'); - } - const archivePattern = `distribution/archives/${task}/build/distributions/elasticsearch-*.${ext}`; const esArchivePath = findMostRecentlyChanged(path.resolve(sourcePath, archivePattern)); @@ -75,9 +63,9 @@ exports.buildSnapshot = async ({ license, sourcePath, log, platform = os.platfor } return esArchivePath; -}; +} -exports.archiveForPlatform = (platform, license) => { +export function archiveForPlatform(platform: NodeJS.Platform, license: string) { const taskPrefix = license === 'oss' ? 'oss-' : ''; switch (platform) { @@ -88,6 +76,6 @@ exports.archiveForPlatform = (platform, license) => { case 'linux': return { format: 'tar', ext: 'tar.gz', task: `${taskPrefix}linux-tar`, platform: 'linux' }; default: - throw new Error(`unknown platform: ${platform}`); + throw new Error(`unsupported platform: ${platform}`); } -}; +} diff --git a/packages/kbn-es/src/utils/cache.js b/packages/kbn-es/src/utils/cache.js deleted file mode 100644 index 248faf23bbc46..0000000000000 --- a/packages/kbn-es/src/utils/cache.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const fs = require('fs'); -const path = require('path'); - -exports.readMeta = function readMeta(file) { - try { - const meta = fs.readFileSync(`${file}.meta`, { - encoding: 'utf8', - }); - - return { - exists: fs.existsSync(file), - ...JSON.parse(meta), - }; - } catch (e) { - if (e.code !== 'ENOENT') { - throw e; - } - - return { - exists: false, - }; - } -}; - -exports.writeMeta = function readMeta(file, details = {}) { - const meta = { - ts: new Date(), - ...details, - }; - - fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync(`${file}.meta`, JSON.stringify(meta, null, 2)); -}; diff --git a/packages/kbn-es/src/utils/cache.ts b/packages/kbn-es/src/utils/cache.ts new file mode 100644 index 0000000000000..819119b6ce010 --- /dev/null +++ b/packages/kbn-es/src/utils/cache.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Fs from 'fs'; +import Path from 'path'; + +export const cache = { + readMeta(path: string) { + try { + const meta = Fs.readFileSync(`${path}.meta`, { + encoding: 'utf8', + }); + + return { + ...JSON.parse(meta), + }; + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + + return {}; + } + }, + + writeMeta(path: string, details = {}) { + const meta = { + ts: new Date(), + ...details, + }; + + Fs.mkdirSync(Path.dirname(path), { recursive: true }); + Fs.writeFileSync(`${path}.meta`, JSON.stringify(meta, null, 2)); + }, +}; diff --git a/packages/kbn-es/src/utils/find_most_recently_changed.test.js b/packages/kbn-es/src/utils/find_most_recently_changed.test.ts similarity index 93% rename from packages/kbn-es/src/utils/find_most_recently_changed.test.js rename to packages/kbn-es/src/utils/find_most_recently_changed.test.ts index 8198495e7197f..721e5baba7513 100644 --- a/packages/kbn-es/src/utils/find_most_recently_changed.test.js +++ b/packages/kbn-es/src/utils/find_most_recently_changed.test.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { findMostRecentlyChanged } from './find_most_recently_changed'; + jest.mock('fs', () => ({ statSync: jest.fn().mockImplementation((path) => { if (path.includes('oldest')) { @@ -31,8 +33,6 @@ jest.mock('fs', () => ({ }), })); -const { findMostRecentlyChanged } = require('./find_most_recently_changed'); - test('returns newest file', () => { const file = findMostRecentlyChanged('/data/*.yml'); expect(file).toEqual('/data/newest.yml'); diff --git a/packages/kbn-es/src/utils/find_most_recently_changed.js b/packages/kbn-es/src/utils/find_most_recently_changed.ts similarity index 65% rename from packages/kbn-es/src/utils/find_most_recently_changed.js rename to packages/kbn-es/src/utils/find_most_recently_changed.ts index 16d300f080b8d..29e1edcc5fcc9 100644 --- a/packages/kbn-es/src/utils/find_most_recently_changed.js +++ b/packages/kbn-es/src/utils/find_most_recently_changed.ts @@ -6,25 +6,22 @@ * Side Public License, v 1. */ -const path = require('path'); -const fs = require('fs'); -const glob = require('glob'); +import path from 'path'; +import fs from 'fs'; +import glob from 'glob'; /** * Find the most recently modified file that matches the pattern pattern - * - * @param {String} pattern absolute path with glob expressions - * @return {String} Absolute path */ -exports.findMostRecentlyChanged = function findMostRecentlyChanged(pattern) { +export function findMostRecentlyChanged(pattern: string) { if (!path.isAbsolute(pattern)) { throw new TypeError(`Pattern must be absolute, got ${pattern}`); } - const ctime = (path) => fs.statSync(path).ctime.getTime(); + const ctime = (p: string) => fs.statSync(p).ctime.getTime(); return glob .sync(pattern) .sort((a, b) => ctime(a) - ctime(b)) .pop(); -}; +} diff --git a/packages/kbn-es/src/utils/index.js b/packages/kbn-es/src/utils/index.js deleted file mode 100644 index ed83495e5310a..0000000000000 --- a/packages/kbn-es/src/utils/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -exports.cache = require('./cache'); -exports.log = require('./log').log; -exports.parseEsLog = require('./parse_es_log').parseEsLog; -exports.findMostRecentlyChanged = require('./find_most_recently_changed').findMostRecentlyChanged; -exports.extractConfigFiles = require('./extract_config_files').extractConfigFiles; -exports.NativeRealm = require('./native_realm').NativeRealm; -exports.buildSnapshot = require('./build_snapshot').buildSnapshot; -exports.archiveForPlatform = require('./build_snapshot').archiveForPlatform; diff --git a/packages/kbn-es/src/utils/index.ts b/packages/kbn-es/src/utils/index.ts new file mode 100644 index 0000000000000..ce0a222dafd3b --- /dev/null +++ b/packages/kbn-es/src/utils/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { cache } from './cache'; +export { log } from './log'; +// @ts-expect-error not typed yet +export { parseEsLog } from './parse_es_log'; +export { findMostRecentlyChanged } from './find_most_recently_changed'; +// @ts-expect-error not typed yet +export { extractConfigFiles } from './extract_config_files'; +// @ts-expect-error not typed yet +export { NativeRealm } from './native_realm'; +export { buildSnapshot } from './build_snapshot'; +export { archiveForPlatform } from './build_snapshot'; diff --git a/packages/kbn-es/src/utils/log.js b/packages/kbn-es/src/utils/log.ts similarity index 80% rename from packages/kbn-es/src/utils/log.js rename to packages/kbn-es/src/utils/log.ts index b33ae509c6c45..a0299f885cf6a 100644 --- a/packages/kbn-es/src/utils/log.js +++ b/packages/kbn-es/src/utils/log.ts @@ -6,11 +6,9 @@ * Side Public License, v 1. */ -const { ToolingLog } = require('@kbn/dev-utils'); +import { ToolingLog } from '@kbn/dev-utils'; -const log = new ToolingLog({ +export const log = new ToolingLog({ level: 'verbose', writeTo: process.stdout, }); - -exports.log = log; diff --git a/packages/kbn-eslint-plugin-eslint/helpers/exports.js b/packages/kbn-eslint-plugin-eslint/helpers/exports.js index b7af8e83d7661..971364633356c 100644 --- a/packages/kbn-eslint-plugin-eslint/helpers/exports.js +++ b/packages/kbn-eslint-plugin-eslint/helpers/exports.js @@ -9,7 +9,7 @@ const Fs = require('fs'); const Path = require('path'); const ts = require('typescript'); -const { REPO_ROOT } = require('@kbn/dev-utils'); +const { REPO_ROOT } = require('@kbn/utils'); const { ExportSet } = require('./export_set'); /** @typedef {import("@typescript-eslint/types").TSESTree.ExportAllDeclaration} ExportAllDeclaration */ diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index a389086c9ee3c..3bd41249e2d51 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -38,10 +38,12 @@ RUNTIME_DEPS = [ "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", "//packages/kbn-utils", + "@npm//@babel/core", "@npm//chalk", "@npm//clean-webpack-plugin", "@npm//compression-webpack-plugin", "@npm//cpy", + "@npm//dedent", "@npm//del", "@npm//execa", "@npm//jest-diff", @@ -64,7 +66,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config:npm_module_types", "//packages/kbn-config-schema:npm_module_types", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-std", "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", @@ -79,7 +81,9 @@ TYPES_DEPS = [ "@npm//pirates", "@npm//rxjs", "@npm//zlib", + "@npm//@types/babel__core", "@npm//@types/compression-webpack-plugin", + "@npm//@types/dedent", "@npm//@types/jest", "@npm//@types/json-stable-stringify", "@npm//@types/js-yaml", diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts b/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts index f00905f3f4920..c07a9764af76f 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts +++ b/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { run, REPO_ROOT } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { OptimizerConfig } from '../optimizer'; import { parseStats, inAnyEntryChunk } from './parse_stats'; diff --git a/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts b/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts index 6f5dabf410ffa..2710ba8a54210 100644 --- a/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts +++ b/packages/kbn-optimizer/src/node/node_auto_tranpilation.ts @@ -39,7 +39,7 @@ import Crypto from 'crypto'; import * as babel from '@babel/core'; import { addHook } from 'pirates'; -import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/dev-utils'; +import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/utils'; import sourceMapSupport from 'source-map-support'; import { Cache } from './cache'; diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.test.ts b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts index d3cc5cceefddf..d1754248dba17 100644 --- a/packages/kbn-optimizer/src/optimizer/get_changes.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts @@ -9,7 +9,8 @@ jest.mock('execa'); import { getChanges } from './get_changes'; -import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const execa: jest.Mock = jest.requireMock('execa'); diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.ts b/packages/kbn-optimizer/src/optimizer/get_changes.ts index c5f8abe99c322..b59f938eb8c37 100644 --- a/packages/kbn-optimizer/src/optimizer/get_changes.ts +++ b/packages/kbn-optimizer/src/optimizer/get_changes.ts @@ -10,7 +10,7 @@ import Path from 'path'; import execa from 'execa'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; export type Changes = Map; diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel index c935d1763dae8..488f09bdd5d52 100644 --- a/packages/kbn-plugin-generator/BUILD.bazel +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -51,7 +51,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-utils", - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "@npm//del", "@npm//execa", "@npm//globby", diff --git a/packages/kbn-plugin-helpers/BUILD.bazel b/packages/kbn-plugin-helpers/BUILD.bazel index d7744aecac26e..47f205f1530b7 100644 --- a/packages/kbn-plugin-helpers/BUILD.bazel +++ b/packages/kbn-plugin-helpers/BUILD.bazel @@ -42,7 +42,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-optimizer", "//packages/kbn-utils", "@npm//del", diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 349719c019c22..fde8deade36b5 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -24,6 +24,7 @@ const VERSION = `${KIBANA_NAMESPACE}.version` as const; // Fields pertaining to the alert const ALERT_ACTION_GROUP = `${ALERT_NAMESPACE}.action_group` as const; +const ALERT_BUILDING_BLOCK_TYPE = `${ALERT_NAMESPACE}.building_block_type` as const; const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; const ALERT_END = `${ALERT_NAMESPACE}.end` as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; @@ -91,6 +92,7 @@ const fields = { TAGS, TIMESTAMP, ALERT_ACTION_GROUP, + ALERT_BUILDING_BLOCK_TYPE, ALERT_DURATION, ALERT_END, ALERT_EVALUATION_THRESHOLD, @@ -141,6 +143,7 @@ const fields = { export { ALERT_ACTION_GROUP, + ALERT_BUILDING_BLOCK_TYPE, ALERT_DURATION, ALERT_END, ALERT_EVALUATION_THRESHOLD, diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index f2a7bf25fb407..5dbe22b56c63f 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -32,6 +32,7 @@ RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", + "//packages/kbn-utils", "@npm//@storybook/addons", "@npm//@storybook/api", "@npm//@storybook/components", @@ -47,9 +48,10 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-ui-shared-deps-npm", "//packages/kbn-ui-shared-deps-src", + "//packages/kbn-utils", "@npm//@storybook/addons", "@npm//@storybook/api", "@npm//@storybook/components", diff --git a/packages/kbn-storybook/src/lib/constants.ts b/packages/kbn-storybook/src/lib/constants.ts index 722f789fde786..69b05c94ea1b0 100644 --- a/packages/kbn-storybook/src/lib/constants.ts +++ b/packages/kbn-storybook/src/lib/constants.ts @@ -7,7 +7,7 @@ */ import { resolve } from 'path'; -import { REPO_ROOT as KIBANA_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT as KIBANA_ROOT } from '@kbn/utils'; export const REPO_ROOT = KIBANA_ROOT; export const ASSET_DIR = resolve(KIBANA_ROOT, 'built_assets/storybook'); diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index 1183de2586424..d2ea3a704f154 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -38,8 +38,9 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-utility-types", + "@npm//tslib", "@npm//@types/glob", "@npm//@types/jest", "@npm//@types/listr", diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 1d1d95d639861..eae0fe2cdf5dc 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -44,11 +44,13 @@ RUNTIME_DEPS = [ "@npm//axios", "@npm//@babel/traverse", "@npm//chance", + "@npm//dedent", "@npm//del", "@npm//enzyme", "@npm//execa", "@npm//exit-hook", "@npm//form-data", + "@npm//getopts", "@npm//globby", "@npm//he", "@npm//history", @@ -59,6 +61,7 @@ RUNTIME_DEPS = [ "@npm//@jest/reporters", "@npm//joi", "@npm//mustache", + "@npm//normalize-path", "@npm//parse-link-header", "@npm//prettier", "@npm//react-dom", @@ -72,13 +75,17 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-dev-utils", + "//packages/kbn-dev-utils:npm_module_types", "//packages/kbn-i18n-react:npm_module_types", + "//packages/kbn-std", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", + "@npm//axios", "@npm//elastic-apm-node", "@npm//del", + "@npm//exit-hook", "@npm//form-data", + "@npm//getopts", "@npm//jest", "@npm//jest-cli", "@npm//jest-snapshot", @@ -86,6 +93,7 @@ TYPES_DEPS = [ "@npm//rxjs", "@npm//xmlbuilder", "@npm//@types/chance", + "@npm//@types/dedent", "@npm//@types/enzyme", "@npm//@types/he", "@npm//@types/history", @@ -93,6 +101,7 @@ TYPES_DEPS = [ "@npm//@types/joi", "@npm//@types/lodash", "@npm//@types/mustache", + "@npm//@types/normalize-path", "@npm//@types/node", "@npm//@types/parse-link-header", "@npm//@types/prettier", diff --git a/packages/kbn-test/src/es/es_test_config.ts b/packages/kbn-test/src/es/es_test_config.ts index db5d705710a75..70000c8068e9f 100644 --- a/packages/kbn-test/src/es/es_test_config.ts +++ b/packages/kbn-test/src/es/es_test_config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { kibanaPackageJson as pkg } from '@kbn/dev-utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Url from 'url'; import { adminTestUser } from '../kbn'; diff --git a/packages/kbn-test/src/failed_tests_reporter/buildkite_metadata.ts b/packages/kbn-test/src/failed_tests_reporter/buildkite_metadata.ts new file mode 100644 index 0000000000000..d63f0166390cb --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/buildkite_metadata.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface BuildkiteMetadata { + buildId?: string; + jobId?: string; + url?: string; + jobName?: string; + jobUrl?: string; +} + +export function getBuildkiteMetadata(): BuildkiteMetadata { + // Buildkite steps that use `parallelism` need a numerical suffix added to identify them + // We should also increment the number by one, since it's 0-based + const jobNumberSuffix = process.env.BUILDKITE_PARALLEL_JOB + ? ` #${parseInt(process.env.BUILDKITE_PARALLEL_JOB, 10) + 1}` + : ''; + + const buildUrl = process.env.BUILDKITE_BUILD_URL; + const jobUrl = process.env.BUILDKITE_JOB_ID + ? `${buildUrl}#${process.env.BUILDKITE_JOB_ID}` + : undefined; + + return { + buildId: process.env.BUJILDKITE_BUILD_ID, + jobId: process.env.BUILDKITE_JOB_ID, + url: buildUrl, + jobUrl, + jobName: process.env.BUILDKITE_LABEL + ? `${process.env.BUILDKITE_LABEL}${jobNumberSuffix}` + : undefined, + }; +} diff --git a/packages/kbn-test/src/failed_tests_reporter/github_api.ts b/packages/kbn-test/src/failed_tests_reporter/github_api.ts index adaae11b7aa16..bb7570225a013 100644 --- a/packages/kbn-test/src/failed_tests_reporter/github_api.ts +++ b/packages/kbn-test/src/failed_tests_reporter/github_api.ts @@ -42,6 +42,7 @@ export class GithubApi { private readonly token: string | undefined; private readonly dryRun: boolean; private readonly x: AxiosInstance; + private requestCount: number = 0; /** * Create a GithubApi helper object, if token is undefined requests won't be @@ -68,6 +69,10 @@ export class GithubApi { }); } + getRequestCount() { + return this.requestCount; + } + private failedTestIssuesPageCache: { pages: GithubIssue[][]; nextRequest: RequestOptions | undefined; @@ -191,53 +196,50 @@ export class GithubApi { }> { const executeRequest = !this.dryRun || options.safeForDryRun; const maxAttempts = options.maxAttempts || 5; - const attempt = options.attempt || 1; - - this.log.verbose('Github API', executeRequest ? 'Request' : 'Dry Run', options); - - if (!executeRequest) { - return { - status: 200, - statusText: 'OK', - headers: {}, - data: dryRunResponse, - }; - } - try { - return await this.x.request(options); - } catch (error) { - const unableToReachGithub = isAxiosRequestError(error); - const githubApiFailed = isAxiosResponseError(error) && error.response.status >= 500; - const errorResponseLog = - isAxiosResponseError(error) && - `[${error.config.method} ${error.config.url}] ${error.response.status} ${error.response.statusText} Error`; + let attempt = 0; + while (true) { + attempt += 1; + this.log.verbose('Github API', executeRequest ? 'Request' : 'Dry Run', options); + + if (!executeRequest) { + return { + status: 200, + statusText: 'OK', + headers: {}, + data: dryRunResponse, + }; + } - if ((unableToReachGithub || githubApiFailed) && attempt < maxAttempts) { - const waitMs = 1000 * attempt; + try { + this.requestCount += 1; + return await this.x.request(options); + } catch (error) { + const unableToReachGithub = isAxiosRequestError(error); + const githubApiFailed = isAxiosResponseError(error) && error.response.status >= 500; + const errorResponseLog = + isAxiosResponseError(error) && + `[${error.config.method} ${error.config.url}] ${error.response.status} ${error.response.statusText} Error`; + + if ((unableToReachGithub || githubApiFailed) && attempt < maxAttempts) { + const waitMs = 1000 * attempt; + + if (errorResponseLog) { + this.log.error(`${errorResponseLog}: waiting ${waitMs}ms to retry`); + } else { + this.log.error(`Unable to reach github, waiting ${waitMs}ms to retry`); + } + + await new Promise((resolve) => setTimeout(resolve, waitMs)); + continue; + } if (errorResponseLog) { - this.log.error(`${errorResponseLog}: waiting ${waitMs}ms to retry`); - } else { - this.log.error(`Unable to reach github, waiting ${waitMs}ms to retry`); + throw new Error(`${errorResponseLog}: ${JSON.stringify(error.response.data)}`); } - await new Promise((resolve) => setTimeout(resolve, waitMs)); - return await this.request( - { - ...options, - maxAttempts, - attempt: attempt + 1, - }, - dryRunResponse - ); + throw error; } - - if (errorResponseLog) { - throw new Error(`${errorResponseLog}: ${JSON.stringify(error.response.data)}`); - } - - throw error; } } } diff --git a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts index e481da019945c..33dab240ec8b4 100644 --- a/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts +++ b/packages/kbn-test/src/failed_tests_reporter/report_failures_to_file.ts @@ -14,6 +14,7 @@ import { ToolingLog } from '@kbn/dev-utils'; import { REPO_ROOT } from '@kbn/utils'; import { escape } from 'he'; +import { BuildkiteMetadata } from './buildkite_metadata'; import { TestFailure } from './get_failures'; const findScreenshots = (dirPath: string, allScreenshots: string[] = []) => { @@ -37,7 +38,11 @@ const findScreenshots = (dirPath: string, allScreenshots: string[] = []) => { return allScreenshots; }; -export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) { +export function reportFailuresToFile( + log: ToolingLog, + failures: TestFailure[], + bkMeta: BuildkiteMetadata +) { if (!failures?.length) { return; } @@ -76,28 +81,15 @@ export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) { .flat() .join('\n'); - // Buildkite steps that use `parallelism` need a numerical suffix added to identify them - // We should also increment the number by one, since it's 0-based - const jobNumberSuffix = process.env.BUILDKITE_PARALLEL_JOB - ? ` #${parseInt(process.env.BUILDKITE_PARALLEL_JOB, 10) + 1}` - : ''; - - const buildUrl = process.env.BUILDKITE_BUILD_URL || ''; - const jobUrl = process.env.BUILDKITE_JOB_ID - ? `${buildUrl}#${process.env.BUILDKITE_JOB_ID}` - : ''; - const failureJSON = JSON.stringify( { ...failure, hash, - buildId: process.env.BUJILDKITE_BUILD_ID || '', - jobId: process.env.BUILDKITE_JOB_ID || '', - url: buildUrl, - jobUrl, - jobName: process.env.BUILDKITE_LABEL - ? `${process.env.BUILDKITE_LABEL}${jobNumberSuffix}` - : '', + buildId: bkMeta.buildId, + jobId: bkMeta.jobId, + url: bkMeta.url, + jobUrl: bkMeta.jobUrl, + jobName: bkMeta.jobName, }, null, 2 @@ -149,11 +141,11 @@ export function reportFailuresToFile(log: ToolingLog, failures: TestFailure[]) {

${ - jobUrl + bkMeta.jobUrl ? `

Buildkite Job
- ${escape(jobUrl)} + ${escape(bkMeta.jobUrl)}

` : '' diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index 193bc668ce003..ecd3007685b76 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -9,7 +9,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { run, createFailError, createFlagError } from '@kbn/dev-utils'; +import { run, createFailError, createFlagError, CiStatsReporter } from '@kbn/dev-utils'; import globby from 'globby'; import normalize from 'normalize-path'; @@ -22,6 +22,7 @@ import { addMessagesToReport } from './add_messages_to_report'; import { getReportMessageIter } from './report_metadata'; import { reportFailuresToEs } from './report_failures_to_es'; import { reportFailuresToFile } from './report_failures_to_file'; +import { getBuildkiteMetadata } from './buildkite_metadata'; const DEFAULT_PATTERNS = [Path.resolve(REPO_ROOT, 'target/junit/**/*.xml')]; @@ -71,108 +72,127 @@ export function runFailedTestsReporterCli() { dryRun: !updateGithub, }); - const buildUrl = flags['build-url'] || (updateGithub ? '' : 'http://buildUrl'); - if (typeof buildUrl !== 'string' || !buildUrl) { - throw createFlagError('Missing --build-url or process.env.BUILD_URL'); - } + const bkMeta = getBuildkiteMetadata(); - const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => - normalize(Path.resolve(p)) - ); - log.info('Searching for reports at', patterns); - const reportPaths = await globby(patterns, { - absolute: true, - }); + try { + const buildUrl = flags['build-url'] || (updateGithub ? '' : 'http://buildUrl'); + if (typeof buildUrl !== 'string' || !buildUrl) { + throw createFlagError('Missing --build-url or process.env.BUILD_URL'); + } - if (!reportPaths.length) { - throw createFailError(`Unable to find any junit reports with patterns [${patterns}]`); - } + const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) => + normalize(Path.resolve(p)) + ); + log.info('Searching for reports at', patterns); + const reportPaths = await globby(patterns, { + absolute: true, + }); - log.info('found', reportPaths.length, 'junit reports', reportPaths); - const newlyCreatedIssues: Array<{ - failure: TestFailure; - newIssue: GithubIssueMini; - }> = []; + if (!reportPaths.length) { + throw createFailError(`Unable to find any junit reports with patterns [${patterns}]`); + } - for (const reportPath of reportPaths) { - const report = await readTestReport(reportPath); - const messages = Array.from(getReportMessageIter(report)); - const failures = await getFailures(report); + log.info('found', reportPaths.length, 'junit reports', reportPaths); + const newlyCreatedIssues: Array<{ + failure: TestFailure; + newIssue: GithubIssueMini; + }> = []; - if (indexInEs) { - await reportFailuresToEs(log, failures); - } + for (const reportPath of reportPaths) { + const report = await readTestReport(reportPath); + const messages = Array.from(getReportMessageIter(report)); + const failures = await getFailures(report); - for (const failure of failures) { - const pushMessage = (msg: string) => { - messages.push({ - classname: failure.classname, - name: failure.name, - message: msg, - }); - }; - - if (failure.likelyIrrelevant) { - pushMessage( - 'Failure is likely irrelevant' + - (updateGithub ? ', so an issue was not created or updated' : '') - ); - continue; + if (indexInEs) { + await reportFailuresToEs(log, failures); } - let existingIssue: GithubIssueMini | undefined = await githubApi.findFailedTestIssue( - (i) => - getIssueMetadata(i.body, 'test.class') === failure.classname && - getIssueMetadata(i.body, 'test.name') === failure.name - ); + for (const failure of failures) { + const pushMessage = (msg: string) => { + messages.push({ + classname: failure.classname, + name: failure.name, + message: msg, + }); + }; + + if (failure.likelyIrrelevant) { + pushMessage( + 'Failure is likely irrelevant' + + (updateGithub ? ', so an issue was not created or updated' : '') + ); + continue; + } - if (!existingIssue) { - const newlyCreated = newlyCreatedIssues.find( - ({ failure: f }) => f.classname === failure.classname && f.name === failure.name + let existingIssue: GithubIssueMini | undefined = await githubApi.findFailedTestIssue( + (i) => + getIssueMetadata(i.body, 'test.class') === failure.classname && + getIssueMetadata(i.body, 'test.name') === failure.name ); - if (newlyCreated) { - existingIssue = newlyCreated.newIssue; + if (!existingIssue) { + const newlyCreated = newlyCreatedIssues.find( + ({ failure: f }) => f.classname === failure.classname && f.name === failure.name + ); + + if (newlyCreated) { + existingIssue = newlyCreated.newIssue; + } } - } - if (existingIssue) { - const newFailureCount = await updateFailureIssue( - buildUrl, - existingIssue, - githubApi, - branch - ); - const url = existingIssue.html_url; - failure.githubIssue = url; - failure.failureCount = updateGithub ? newFailureCount : newFailureCount - 1; - pushMessage(`Test has failed ${newFailureCount - 1} times on tracked branches: ${url}`); - if (updateGithub) { - pushMessage(`Updated existing issue: ${url} (fail count: ${newFailureCount})`); + if (existingIssue) { + const newFailureCount = await updateFailureIssue( + buildUrl, + existingIssue, + githubApi, + branch + ); + const url = existingIssue.html_url; + failure.githubIssue = url; + failure.failureCount = updateGithub ? newFailureCount : newFailureCount - 1; + pushMessage( + `Test has failed ${newFailureCount - 1} times on tracked branches: ${url}` + ); + if (updateGithub) { + pushMessage(`Updated existing issue: ${url} (fail count: ${newFailureCount})`); + } + continue; } - continue; - } - const newIssue = await createFailureIssue(buildUrl, failure, githubApi, branch); - pushMessage('Test has not failed recently on tracked branches'); - if (updateGithub) { - pushMessage(`Created new issue: ${newIssue.html_url}`); - failure.githubIssue = newIssue.html_url; + const newIssue = await createFailureIssue(buildUrl, failure, githubApi, branch); + pushMessage('Test has not failed recently on tracked branches'); + if (updateGithub) { + pushMessage(`Created new issue: ${newIssue.html_url}`); + failure.githubIssue = newIssue.html_url; + } + newlyCreatedIssues.push({ failure, newIssue }); + failure.failureCount = updateGithub ? 1 : 0; } - newlyCreatedIssues.push({ failure, newIssue }); - failure.failureCount = updateGithub ? 1 : 0; - } - // mutates report to include messages and writes updated report to disk - await addMessagesToReport({ - report, - messages, - log, - reportPath, - dryRun: !flags['report-update'], - }); + // mutates report to include messages and writes updated report to disk + await addMessagesToReport({ + report, + messages, + log, + reportPath, + dryRun: !flags['report-update'], + }); - reportFailuresToFile(log, failures); + reportFailuresToFile(log, failures, bkMeta); + } + } finally { + await CiStatsReporter.fromEnv(log).metrics([ + { + group: 'github api request count', + id: `failed test reporter`, + value: githubApi.getRequestCount(), + meta: Object.fromEntries( + Object.entries(bkMeta).map( + ([k, v]) => [`buildkite${k[0].toUpperCase()}${k.slice(1)}`, v] as const + ) + ), + }, + ]); } }, { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js index 3446c5be5d4a7..4f798839d7231 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js @@ -8,7 +8,7 @@ import Path from 'path'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; /** * Traverse the suites configured and ensure that each suite has no more than one ciGroup assigned diff --git a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts index e87f316a100a7..53ce4c74c1388 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts @@ -14,7 +14,7 @@ jest.mock('@kbn/utils', () => { return { REPO_ROOT: '/dev/null/root' }; }); -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Lifecycle } from './lifecycle'; import { SuiteTracker } from './suite_tracker'; import { Suite } from '../fake_mocha_types'; diff --git a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js index 03947f7e267ba..63d2b56350ba1 100644 --- a/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js +++ b/packages/kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js @@ -9,7 +9,7 @@ const Fs = require('fs'); const Path = require('path'); -const { REPO_ROOT: REPO_ROOT_FOLLOWING_SYMLINKS } = require('@kbn/dev-utils'); +const { REPO_ROOT: REPO_ROOT_FOLLOWING_SYMLINKS } = require('@kbn/utils'); const BASE_REPO_ROOT = Path.resolve( Fs.realpathSync(Path.resolve(REPO_ROOT_FOLLOWING_SYMLINKS, 'package.json')), '..' diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 6dde114d3a98e..6a6c7edb98c79 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -9,7 +9,8 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; import { startWith, switchMap, take } from 'rxjs/operators'; -import { withProcRunner, ToolingLog, REPO_ROOT, getTimeReporter } from '@kbn/dev-utils'; +import { withProcRunner, ToolingLog, getTimeReporter } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import dedent from 'dedent'; import { diff --git a/packages/kbn-test/src/kbn/users.ts b/packages/kbn-test/src/kbn/users.ts index 230354089dcac..88480fde74ddc 100644 --- a/packages/kbn-test/src/kbn/users.ts +++ b/packages/kbn-test/src/kbn/users.ts @@ -14,7 +14,7 @@ export const kibanaTestUser = { }; export const kibanaServerTestUser = { - username: env.TEST_KIBANA_SERVER_USER || 'kibana', + username: env.TEST_KIBANA_SERVER_USER || 'kibana_system', password: env.TEST_KIBANA_SERVER_PASS || 'changeme', }; diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index 4adae7d1cd031..6da34228bbe7f 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -12,7 +12,8 @@ import { existsSync } from 'fs'; import Path from 'path'; import FormData from 'form-data'; -import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index b347915ae3310..d759948a6c576 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -41,10 +41,10 @@ TYPES_DEPS = [ "@npm//query-string", "@npm//utility-types", "@npm//@types/jest", - "@npm//@types/history", "@npm//@types/node", "@npm//@types/react-router-config", "@npm//@types/react-router-dom", + "@npm//@types/history", ] jsts_transpiler( diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index e82fcf791804e..ac337f8bb5b87 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -267,7 +267,6 @@ describe('createRouter', () => { const matches = router.matchRoutes('/', history.location); - // @ts-expect-error 4.3.5 upgrade - router doesn't seem able to merge properly when two routes match expect(matches[1]?.match.params).toEqual({ query: { rangeFrom: 'now-30m', @@ -286,7 +285,6 @@ describe('createRouter', () => { expect(matchedRoutes.length).toEqual(4); - // @ts-expect-error 4.3.5 upgrade - router doesn't seem able to merge properly when two routes match expect(matchedRoutes[matchedRoutes.length - 1].match).toEqual({ isExact: true, params: { diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 186f949d9c8e8..89ff4fc6b0c6c 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -23,7 +23,7 @@ function toReactRouterPath(path: string) { return path.replace(/(?:{([^\/]+)})/g, ':$1'); } -export function createRouter(routes: TRoute[]): Router { +export function createRouter(routes: TRoutes): Router { const routesByReactRouterConfig = new Map(); const reactRouterConfigsByRoute = new Map(); @@ -181,8 +181,10 @@ export function createRouter(routes: TRoute[]): Router { + return link(path, ...args); + }, getParams: (...args: any[]) => { const matches = matchRoutes(...args); return matches.length @@ -195,13 +197,11 @@ export function createRouter(routes: TRoute[]): Router { return matchRoutes(...args) as any; }, - getRoutePath: (route: Route) => { + getRoutePath: (route) => { return reactRouterConfigsByRoute.get(route)!.path as string; }, getRoutesToMatch: (path: string) => { - return getRoutesToMatch(path) as unknown as FlattenRoutesOf; + return getRoutesToMatch(path) as unknown as FlattenRoutesOf; }, }; - - return router; } diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 3c09b60054a0c..c1ae5afd816ee 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -115,7 +115,7 @@ export interface RouteMatch { params: t.Type; } ? t.TypeOf - : AnyObj; + : {}; }; } @@ -160,11 +160,10 @@ interface ReadonlyPlainRoute { } export type Route = PlainRoute | ReadonlyPlainRoute; -type AnyObj = Record; interface DefaultOutput { - path: AnyObj; - query: AnyObj; + path: {}; + query: {}; } type OutputOfRouteMatch = TRouteMatch extends { @@ -191,21 +190,20 @@ type TypeOfRouteMatch = TRouteMatch extends { route: { params: t.Type }; } ? t.TypeOf - : AnyObj; + : {}; type TypeOfMatches = TRouteMatches extends [RouteMatch] ? TypeOfRouteMatch : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] ? TypeOfRouteMatch & - (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : AnyObj) - : AnyObj; + (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : {}) + : {}; export type TypeOf< TRoutes extends Route[], TPath extends PathsOf, TWithDefaultOutput extends boolean = true -> = TypeOfMatches> & - (TWithDefaultOutput extends true ? DefaultOutput : AnyObj); +> = TypeOfMatches> & (TWithDefaultOutput extends true ? DefaultOutput : {}); export type TypeAsArgs = keyof TObject extends never ? [] @@ -278,7 +276,7 @@ type MapRoute = MaybeUnion< >; } > - : AnyObj + : {} >; type MapRoutes = TRoutes extends [Route] @@ -343,7 +341,7 @@ type MapRoutes = TRoutes extends [Route] MapRoute & MapRoute & MapRoute - : AnyObj; + : {}; // const element = null as any; diff --git a/src/cli/serve/integration_tests/invalid_config.test.ts b/src/cli/serve/integration_tests/invalid_config.test.ts index 2de902582a548..ca051f37a816e 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.ts +++ b/src/cli/serve/integration_tests/invalid_config.test.ts @@ -8,7 +8,7 @@ import { spawnSync } from 'child_process'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const INVALID_CONFIG_PATH = require.resolve('./__fixtures__/invalid_config.yml'); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 692367cd0f580..fed3aa3093166 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -486,6 +486,7 @@ export class DocLinksService { hdfsRepo: `${PLUGIN_DOCS}repository-hdfs.html`, s3Repo: `${PLUGIN_DOCS}repository-s3.html`, snapshotRestoreRepos: `${PLUGIN_DOCS}repository.html`, + mapperSize: `${PLUGIN_DOCS}mapper-size-usage.html`, }, snapshotRestore: { guide: `${ELASTICSEARCH_DOCS}snapshot-restore.html`, @@ -874,7 +875,14 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; - readonly plugins: Record; + readonly plugins: { + azureRepo: string; + gcsRepo: string; + hdfsRepo: string; + s3Repo: string; + snapshotRestoreRepos: string; + mapperSize: string; + }; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 08d41ab1301b0..63e0898b5fb90 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -773,7 +773,14 @@ export interface DocLinksStart { }>; readonly watcher: Record; readonly ccs: Record; - readonly plugins: Record; + readonly plugins: { + azureRepo: string; + gcsRepo: string; + hdfsRepo: string; + s3Repo: string; + snapshotRestoreRepos: string; + mapperSize: string; + }; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts index 2e80fbb9d20c0..c1f6ffb5add77 100644 --- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts +++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts @@ -7,7 +7,7 @@ */ import supertest from 'supertest'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { HttpService, InternalHttpServicePreboot, InternalHttpServiceSetup } from '../../http'; import { contextServiceMock } from '../../context/context_service.mock'; import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index ddb87d31383c8..4d7b4e1ba5548 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { CoreContext } from './core_context'; import { Env, IConfigService } from './config'; diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 3b75d19b80a10..ce5672ad30519 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -21,7 +21,7 @@ import { MockClusterClient, isScriptingEnabledMock } from './elasticsearch_servi import type { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '../config'; import { configServiceMock, getEnvOptions } from '../config/mocks'; import { CoreContext } from '../core_context'; diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts index ad05d37c81e99..8e2cd58733faf 100644 --- a/src/core/server/http/cookie_session_storage.test.ts +++ b/src/core/server/http/cookie_session_storage.test.ts @@ -8,7 +8,7 @@ import { parse as parseCookie } from 'tough-cookie'; import supertest from 'supertest'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { BehaviorSubject } from 'rxjs'; diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 4955d19668580..3a387cdfd5e35 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -10,7 +10,7 @@ import { mockHttpServer } from './http_service.test.mocks'; import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../config/mocks'; import { HttpService } from '.'; import { HttpConfigType, config } from './http_config'; diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts index 4e1a88e967f8f..8a8c545b365b3 100644 --- a/src/core/server/http/test_utils.ts +++ b/src/core/server/http/test_utils.ts @@ -8,7 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import moment from 'moment'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { Env } from '../config'; import { HttpService } from './http_service'; diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts index cba188c94c74e..3fd3c4a7a24d6 100644 --- a/src/core/server/metrics/logging/get_ops_metrics_log.test.ts +++ b/src/core/server/metrics/logging/get_ops_metrics_log.test.ts @@ -42,6 +42,7 @@ const testMetrics = { memory: { heap: { used_in_bytes: 100 } }, uptime_in_millis: 1500, event_loop_delay: 50, + event_loop_delay_histogram: { percentiles: { '50': 50, '75': 75, '95': 95, '99': 99 } }, }, os: { load: { @@ -56,7 +57,7 @@ describe('getEcsOpsMetricsLog', () => { it('provides correctly formatted message', () => { const result = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics)); expect(result.message).toMatchInlineSnapshot( - `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] delay: 50.000"` + `"memory: 100.0B uptime: 0:00:01 load: [10.00,20.00,30.00] mean delay: 50.000 delay histogram: { 50: 50.000; 95: 95.000; 99: 99.000 }"` ); }); @@ -70,6 +71,7 @@ describe('getEcsOpsMetricsLog', () => { const missingMetrics = { ...baseMetrics, process: {}, + processes: [], os: {}, } as unknown as OpsMetrics; const logMeta = getEcsOpsMetricsLog(missingMetrics); @@ -77,39 +79,41 @@ describe('getEcsOpsMetricsLog', () => { }); it('provides an ECS-compatible response', () => { - const logMeta = getEcsOpsMetricsLog(createBaseOpsMetrics()); - expect(logMeta).toMatchInlineSnapshot(` + const logMeta = getEcsOpsMetricsLog(createMockOpsMetrics(testMetrics)); + expect(logMeta.meta).toMatchInlineSnapshot(` Object { - "message": "memory: 1.0B load: [1.00,1.00,1.00] delay: 1.000", - "meta": Object { - "event": Object { - "category": Array [ - "process", - "host", - ], - "kind": "metric", - "type": Array [ - "info", - ], - }, - "host": Object { - "os": Object { - "load": Object { - "15m": 1, - "1m": 1, - "5m": 1, - }, + "event": Object { + "category": Array [ + "process", + "host", + ], + "kind": "metric", + "type": Array [ + "info", + ], + }, + "host": Object { + "os": Object { + "load": Object { + "15m": 30, + "1m": 10, + "5m": 20, }, }, - "process": Object { - "eventLoopDelay": 1, - "memory": Object { - "heap": Object { - "usedInBytes": 1, - }, + }, + "process": Object { + "eventLoopDelay": 50, + "eventLoopDelayHistogram": Object { + "50": 50, + "95": 95, + "99": 99, + }, + "memory": Object { + "heap": Object { + "usedInBytes": 100, }, - "uptime": 0, }, + "uptime": 1, }, } `); diff --git a/src/core/server/metrics/logging/get_ops_metrics_log.ts b/src/core/server/metrics/logging/get_ops_metrics_log.ts index 7e13f35889ec7..6211407ae86f0 100644 --- a/src/core/server/metrics/logging/get_ops_metrics_log.ts +++ b/src/core/server/metrics/logging/get_ops_metrics_log.ts @@ -30,10 +30,29 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) { // HH:mm:ss message format for backward compatibility const uptimeValMsg = uptimeVal ? `uptime: ${numeral(uptimeVal).format('00:00:00')} ` : ''; - // Event loop delay is in ms + // Event loop delay metrics are in ms const eventLoopDelayVal = process?.event_loop_delay; const eventLoopDelayValMsg = eventLoopDelayVal - ? `delay: ${numeral(process?.event_loop_delay).format('0.000')}` + ? `mean delay: ${numeral(process?.event_loop_delay).format('0.000')}` + : ''; + + const eventLoopDelayPercentiles = process?.event_loop_delay_histogram?.percentiles; + + // Extract 50th, 95th and 99th percentiles for log meta + const eventLoopDelayHistVals = eventLoopDelayPercentiles + ? { + 50: eventLoopDelayPercentiles[50], + 95: eventLoopDelayPercentiles[95], + 99: eventLoopDelayPercentiles[99], + } + : undefined; + // Format message from 50th, 95th and 99th percentiles + const eventLoopDelayHistMsg = eventLoopDelayPercentiles + ? ` delay histogram: { 50: ${numeral(eventLoopDelayPercentiles['50']).format( + '0.000' + )}; 95: ${numeral(eventLoopDelayPercentiles['95']).format('0.000')}; 99: ${numeral( + eventLoopDelayPercentiles['99'] + ).format('0.000')} }` : ''; const loadEntries = { @@ -65,6 +84,7 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) { }, }, eventLoopDelay: eventLoopDelayVal, + eventLoopDelayHistogram: eventLoopDelayHistVals, }, host: { os: { @@ -75,7 +95,13 @@ export function getEcsOpsMetricsLog(metrics: OpsMetrics) { }; return { - message: `${processMemoryUsedInBytesMsg}${uptimeValMsg}${loadValsMsg}${eventLoopDelayValMsg}`, + message: [ + processMemoryUsedInBytesMsg, + uptimeValMsg, + loadValsMsg, + eventLoopDelayValMsg, + eventLoopDelayHistMsg, + ].join(''), meta, }; } diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index d7de41fd7ccf7..27043b8fa2c8a 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -203,6 +203,7 @@ describe('MetricsService', () => { }, "process": Object { "eventLoopDelay": undefined, + "eventLoopDelayHistogram": undefined, "memory": Object { "heap": Object { "usedInBytes": undefined, diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 958e051d0476d..a6ffdff4422be 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -7,7 +7,7 @@ */ // must be before mocks imports to avoid conflicting with `REPO_ROOT` accessor. -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { mockPackage, scanPluginSearchPathsMock } from './plugins_discovery.test.mocks'; import mockFs from 'mock-fs'; import { loggingSystemMock } from '../../logging/logging_system.mock'; diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts index 4170d9422f277..ebbb3fa473b6d 100644 --- a/src/core/server/plugins/integration_tests/plugins_service.test.ts +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -7,7 +7,7 @@ */ // must be before mocks imports to avoid conflicting with `REPO_ROOT` accessor. -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { mockPackage, mockDiscover } from './plugins_service.test.mocks'; import { join } from 'path'; diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 513e893992005..92cbda2a69cfe 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -8,7 +8,7 @@ import { join } from 'path'; import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; import { Env } from '../config'; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 867d4d978314b..7bcf392ed510b 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -8,7 +8,7 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { fromRoot } from '@kbn/utils'; import { createPluginInitializerContext, diff --git a/src/core/server/plugins/plugins_config.test.ts b/src/core/server/plugins/plugins_config.test.ts index d65b057fb65c0..b9225054e63ef 100644 --- a/src/core/server/plugins/plugins_config.test.ts +++ b/src/core/server/plugins/plugins_config.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../config/mocks'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { Env } from '../config'; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 0c077d732c67b..5a05817d2111f 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -11,7 +11,8 @@ import { mockDiscover, mockPackage } from './plugins_service.test.mocks'; import { resolve, join } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@kbn/config-schema'; -import { createAbsolutePathSerializer, REPO_ROOT } from '@kbn/dev-utils'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ConfigPath, ConfigService, Env } from '../config'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 4cd8e4c551bea..3d8a47005b362 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -14,7 +14,7 @@ import { import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '../config'; import { configServiceMock, getEnvOptions } from '../config/mocks'; import { CoreContext } from '../core_context'; diff --git a/src/core/server/preboot/preboot_service.test.ts b/src/core/server/preboot/preboot_service.test.ts index dd4b1cb7d1df0..77242f0c5765f 100644 --- a/src/core/server/preboot/preboot_service.test.ts +++ b/src/core/server/preboot/preboot_service.test.ts @@ -7,7 +7,7 @@ */ import { nextTick } from '@kbn/test/jest'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { LoggerFactory } from '@kbn/logging'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../config/mocks'; diff --git a/src/core/server/root/index.test.ts b/src/core/server/root/index.test.ts index 7eba051a128f0..6ea3e05b9c2c2 100644 --- a/src/core/server/root/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -10,7 +10,7 @@ import { rawConfigService, configService, logger, mockServer } from './index.tes import { BehaviorSubject } from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../config/mocks'; import { Root } from '.'; import { Env } from '../config'; diff --git a/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts index c22c6154c2605..139cd298d28ed 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/7.7.2_xpack_100k.test.ts @@ -8,7 +8,7 @@ import path from 'path'; import { unlink } from 'fs/promises'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../../../config/mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; diff --git a/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts index 0ed9262017263..c341463b78910 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/migration_from_older_v1.test.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import Util from 'util'; import Semver from 'semver'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../../../config/mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; diff --git a/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts b/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts index 15d985daccba6..34d1317755c14 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/migration_from_same_v1.test.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import Util from 'util'; import Semver from 'semver'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Env } from '@kbn/config'; import { getEnvOptions } from '../../../config/mocks'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index a4f6c019c9624..a8bda95af46f9 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -19,7 +19,7 @@ import { import { BehaviorSubject } from 'rxjs'; import { RawPackageInfo } from '@kbn/config'; import { ByteSizeValue } from '@kbn/config-schema'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { SavedObjectsService } from './saved_objects_service'; import { mockCoreContext } from '../core_context.mock'; diff --git a/src/core/server/saved_objects/service/lib/repository.test.ts b/src/core/server/saved_objects/service/lib/repository.test.ts index ab692b146e7f6..1668df7a82253 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.ts @@ -3558,6 +3558,20 @@ describe('SavedObjectsRepository', () => { }); }); + it('search for the right fields when typeToNamespacesMap is set', async () => { + const relevantOpts = { + ...commonOptions, + fields: ['title'], + type: '', + namespaces: [], + typeToNamespacesMap: new Map([[type, [namespace]]]), + }; + + await findSuccess(relevantOpts, namespace); + const esOptions = client.search.mock.calls[0][0]; + expect(esOptions?._source ?? []).toContain('index-pattern.title'); + }); + it(`accepts hasReferenceOperator`, async () => { const relevantOpts: SavedObjectsFindOptions = { ...commonOptions, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 0d17525016043..53bc6f158bf93 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -930,7 +930,7 @@ export class SavedObjectsRepository { index: pit ? undefined : this.getIndicesForTypes(allowedTypes), // If `searchAfter` is provided, we drop `from` as it will not be used for pagination. from: searchAfter ? undefined : perPage * (page - 1), - _source: includedFields(type, fields), + _source: includedFields(allowedTypes, fields), preference, rest_total_hits_as_int: true, size: perPage, @@ -938,7 +938,7 @@ export class SavedObjectsRepository { size: perPage, seq_no_primary_term: true, from: perPage * (page - 1), - _source: includedFields(type, fields), + _source: includedFields(allowedTypes, fields), ...(aggsObject ? { aggs: aggsObject } : {}), ...getSearchDsl(this._mappings, this._registry, { search, diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 112693aae0279..48547883d5f67 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -26,7 +26,7 @@ import { } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { rawConfigServiceMock, getEnvOptions } from './config/mocks'; import { Env } from './config'; import { Server } from './server'; diff --git a/src/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index ef635e90dac70..3f85beb2acec6 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -7,7 +7,7 @@ */ import { Env } from '@kbn/config'; -import { REPO_ROOT } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../../config/mocks'; import { startServers, stopServers } from './lib'; import { docExistsSuite } from './doc_exists'; diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 58720be637e2f..c326c7a35df63 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { createTestEsCluster, CreateTestEsClusterOptions, diff --git a/src/dev/build/lib/integration_tests/version_info.test.ts b/src/dev/build/lib/integration_tests/version_info.test.ts index e7a3a04c04734..9385de6e00a4f 100644 --- a/src/dev/build/lib/integration_tests/version_info.test.ts +++ b/src/dev/build/lib/integration_tests/version_info.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { kibanaPackageJson as pkg } from '@kbn/dev-utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import { getVersionInfo } from '../version_info'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts index 02b469820f900..cc1ffb5f3e301 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/bundle_dockerfiles.ts @@ -10,7 +10,8 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { copyFile } from 'fs/promises'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import Mustache from 'mustache'; import { compressTar, copyAll, mkdirp, write, Config } from '../../../lib'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 6a192baed3fa3..085b4393caa66 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -10,7 +10,8 @@ import { access, link, unlink, chmod } from 'fs'; import { resolve, basename } from 'path'; import { promisify } from 'util'; -import { ToolingLog, kibanaPackageJson } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { kibanaPackageJson } from '@kbn/utils'; import { write, copyAll, mkdirp, exec, Config, Build } from '../../../lib'; import * as dockerTemplates from './templates'; diff --git a/src/dev/chromium_version.ts b/src/dev/chromium_version.ts index 410fcc72fbc0f..1f55330a92bb6 100644 --- a/src/dev/chromium_version.ts +++ b/src/dev/chromium_version.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; +import { run, ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import chalk from 'chalk'; import cheerio from 'cheerio'; import fs from 'fs'; diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 57467d84f1f61..40d36ed46ea34 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -7,7 +7,8 @@ */ import { enumeratePatterns } from '../team_assignment/enumerate_patterns'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const log = new ToolingLog({ level: 'info', diff --git a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js index 0e341a3aac1dc..a38c4ee50b40a 100644 --- a/src/dev/code_coverage/ingest_coverage/team_assignment/index.js +++ b/src/dev/code_coverage/ingest_coverage/team_assignment/index.js @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { run, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { parse } from './parse_owners'; import { flush } from './flush'; import { enumeratePatterns } from './enumerate_patterns'; diff --git a/src/dev/ensure_all_tests_in_ci_group.ts b/src/dev/ensure_all_tests_in_ci_group.ts index aeccefae05d2c..a2d9729d3352b 100644 --- a/src/dev/ensure_all_tests_in_ci_group.ts +++ b/src/dev/ensure_all_tests_in_ci_group.ts @@ -12,7 +12,8 @@ import Fs from 'fs/promises'; import execa from 'execa'; import { safeLoad } from 'js-yaml'; -import { run, REPO_ROOT } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; const RELATIVE_JOBS_YAML_PATH = '.ci/ci_groups.yml'; diff --git a/src/dev/eslint/run_eslint_with_types.ts b/src/dev/eslint/run_eslint_with_types.ts index 750011dea1031..0f2a10d07d681 100644 --- a/src/dev/eslint/run_eslint_with_types.ts +++ b/src/dev/eslint/run_eslint_with_types.ts @@ -14,7 +14,8 @@ import execa from 'execa'; import * as Rx from 'rxjs'; import { mergeMap, reduce } from 'rxjs/operators'; import { supportsColor } from 'chalk'; -import { REPO_ROOT, run, createFailError } from '@kbn/dev-utils'; +import { run, createFailError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; import { PROJECTS } from '../typescript/projects'; diff --git a/src/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts index f1725f34d1f8e..53a53bc08e15b 100644 --- a/src/dev/plugin_discovery/find_plugins.ts +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -8,11 +8,9 @@ import Path from 'path'; import { getPluginSearchPaths } from '@kbn/config'; -import { - KibanaPlatformPlugin, - REPO_ROOT, - simpleKibanaPlatformPluginDiscovery, -} from '@kbn/dev-utils'; +import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; + +import { REPO_ROOT } from '@kbn/utils'; export interface SearchOptions { oss: boolean; diff --git a/src/dev/run_build_docs_cli.ts b/src/dev/run_build_docs_cli.ts index aad524b4437d3..8ee75912c1a7e 100644 --- a/src/dev/run_build_docs_cli.ts +++ b/src/dev/run_build_docs_cli.ts @@ -9,7 +9,8 @@ import Path from 'path'; import dedent from 'dedent'; -import { run, REPO_ROOT, createFailError } from '@kbn/dev-utils'; +import { run, createFailError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; const DEFAULT_DOC_REPO_PATH = Path.resolve(REPO_ROOT, '..', 'docs'); diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index f7974b464fcaf..f9ee7bd84c54f 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -10,7 +10,8 @@ import dedent from 'dedent'; import { parseDependencyTree, parseCircular, prettyCircular } from 'dpdm'; import { relative } from 'path'; import { getPluginSearchPaths } from '@kbn/config'; -import { REPO_ROOT, run } from '@kbn/dev-utils'; +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; interface Options { debug?: boolean; diff --git a/src/dev/run_precommit_hook.js b/src/dev/run_precommit_hook.js index a7bd0a9f57f6e..dfa3a94426bb2 100644 --- a/src/dev/run_precommit_hook.js +++ b/src/dev/run_precommit_hook.js @@ -8,7 +8,8 @@ import SimpleGit from 'simple-git/promise'; -import { run, combineErrors, createFlagError, REPO_ROOT } from '@kbn/dev-utils'; +import { run, combineErrors, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import * as Eslint from './eslint'; import * as Stylelint from './stylelint'; import { getFilesForCommit, checkFileCasing } from './precommit_hook'; diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts index aaa8c0d12fa4d..f3896cf676e27 100644 --- a/src/dev/typescript/build_ts_refs.ts +++ b/src/dev/typescript/build_ts_refs.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { ToolingLog, REPO_ROOT, ProcRunner } from '@kbn/dev-utils'; +import { ToolingLog, ProcRunner } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { ROOT_REFS_CONFIG_PATH } from './root_refs_config'; import { Project } from './project'; diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index c68424c2a98f7..09866315fc8dd 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -8,7 +8,8 @@ import Path from 'path'; -import { run, REPO_ROOT, createFlagError } from '@kbn/dev-utils'; +import { run, createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import del from 'del'; import { RefOutputCache } from './ref_output_cache'; diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts index b7e641ceb33d5..32b08ec1ba0df 100644 --- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts +++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts @@ -9,7 +9,8 @@ import Path from 'path'; import Fs from 'fs/promises'; -import { ToolingLog, kibanaPackageJson, extract } from '@kbn/dev-utils'; +import { ToolingLog, extract } from '@kbn/dev-utils'; +import { kibanaPackageJson } from '@kbn/utils'; import del from 'del'; import tempy from 'tempy'; diff --git a/src/dev/typescript/root_refs_config.ts b/src/dev/typescript/root_refs_config.ts index f4aa88f1ea6b2..e20b1ab46cd82 100644 --- a/src/dev/typescript/root_refs_config.ts +++ b/src/dev/typescript/root_refs_config.ts @@ -10,7 +10,8 @@ import Path from 'path'; import Fs from 'fs/promises'; import dedent from 'dedent'; -import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import normalize from 'normalize-path'; import { PROJECTS } from './projects'; diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss index 6e1afd91c476d..fb004dfce4ec0 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss +++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/index.scss @@ -9,14 +9,6 @@ padding: $euiSizeS; } -.heatmap-chart__empty { - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - .heatmap-chart-icon__subdued { fill: $euiTextSubduedColor; } diff --git a/src/plugins/charts/public/static/components/empty_placeholder.scss b/src/plugins/charts/public/static/components/empty_placeholder.scss new file mode 100644 index 0000000000000..3f98da9eecb6a --- /dev/null +++ b/src/plugins/charts/public/static/components/empty_placeholder.scss @@ -0,0 +1,7 @@ +.chart__empty-placeholder { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/plugins/charts/public/static/components/empty_placeholder.tsx b/src/plugins/charts/public/static/components/empty_placeholder.tsx index db3f3fb6739d5..e376120c9cd9e 100644 --- a/src/plugins/charts/public/static/components/empty_placeholder.tsx +++ b/src/plugins/charts/public/static/components/empty_placeholder.tsx @@ -9,15 +9,20 @@ import React from 'react'; import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import './empty_placeholder.scss'; -export const EmptyPlaceholder = (props: { icon: IconType }) => ( +export const EmptyPlaceholder = ({ + icon, + message = , +}: { + icon: IconType; + message?: JSX.Element; +}) => ( <> - - + + -

- -

+

{message}

); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js index 866e19a1d0d3e..efd7dbd088581 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js @@ -2022,22 +2022,37 @@ ace.define( }, // parses and returns the method method = function () { - const [first, ...rest] = text.split(' '); - text = first.toUpperCase() + rest.join(' '); - ch = ch.toUpperCase(); - switch (ch) { + case 'g': + next('g'); + next('e'); + next('t'); + return 'get'; case 'G': next('G'); next('E'); next('T'); return 'GET'; + case 'h': + next('h'); + next('e'); + next('a'); + next('d'); + return 'head'; case 'H': next('H'); next('E'); next('A'); next('D'); return 'HEAD'; + case 'd': + next('d'); + next('e'); + next('l'); + next('e'); + next('t'); + next('e'); + return 'delete'; case 'D': next('D'); next('E'); @@ -2046,6 +2061,22 @@ ace.define( next('T'); next('E'); return 'DELETE'; + case 'p': + next('p'); + switch (ch) { + case 'u': + next('u'); + next('t'); + return 'put'; + case 'o': + next('o'); + next('s'); + next('t'); + return 'post'; + default: + error('Unexpected \'' + ch + '\''); + } + break; case 'P': next('P'); switch (ch) { diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index d4996f9fd8862..2a4ee6b2e346b 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -250,7 +250,10 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { // Fetch autocomplete info if setting is set to true, and if user has made changes. if (settingsToRetrieve[settingsKey] === true) { - return es.send('GET', settingKeyToPathMap[settingsKey], null, true); + // Use pretty=false in these request in order to compress the response by removing whitespace + const path = `${settingKeyToPathMap[settingsKey]}?pretty=false`; + + return es.send('GET', path, null, true); } else { const settingsPromise = new $.Deferred(); if (settingsToRetrieve[settingsKey] === false) { diff --git a/src/plugins/dashboard/public/application/lib/filter_utils.ts b/src/plugins/dashboard/public/application/lib/filter_utils.ts index a31b83ec2df8f..c6b9ae2d01cf3 100644 --- a/src/plugins/dashboard/public/application/lib/filter_utils.ts +++ b/src/plugins/dashboard/public/application/lib/filter_utils.ts @@ -72,7 +72,7 @@ export const cleanFiltersForComparison = (filters: Filter[]) => { export const cleanFiltersForSerialize = (filters: Filter[]): Filter[] => { return filters.map((filter) => { - if (filter.meta.value) { + if (filter.meta?.value) { delete filter.meta.value; } return filter; diff --git a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts index 31579e92bd1ec..03a03842c0e66 100644 --- a/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_saved_dashboard_state.ts @@ -8,10 +8,10 @@ import _ from 'lodash'; +import { getDashboard60Warning, dashboardLoadingErrorStrings } from '../../dashboard_strings'; import { savedObjectToDashboardState } from './convert_dashboard_state'; import { DashboardState, DashboardBuildContext } from '../../types'; import { DashboardConstants, DashboardSavedObject } from '../..'; -import { getDashboard60Warning } from '../../dashboard_strings'; import { migrateLegacyQuery } from './migrate_legacy_query'; import { cleanFiltersForSerialize } from './filter_utils'; import { ViewMode } from '../../services/embeddable'; @@ -52,34 +52,33 @@ export const loadSavedDashboardState = async ({ return; } await indexPatterns.ensureDefaultDataView(); - let savedDashboard: DashboardSavedObject | undefined; try { - savedDashboard = (await savedDashboards.get({ + const savedDashboard = (await savedDashboards.get({ id: savedDashboardId, useResolve: true, })) as DashboardSavedObject; + const savedDashboardState = savedObjectToDashboardState({ + savedDashboard, + usageCollection, + showWriteControls, + savedObjectsTagging, + version: initializerContext.env.packageInfo.version, + }); + + const isViewMode = !showWriteControls || Boolean(savedDashboard.id); + savedDashboardState.viewMode = isViewMode ? ViewMode.VIEW : ViewMode.EDIT; + savedDashboardState.filters = cleanFiltersForSerialize(savedDashboardState.filters); + savedDashboardState.query = migrateLegacyQuery( + savedDashboardState.query || queryString.getDefaultQuery() + ); + + return { savedDashboardState, savedDashboard }; } catch (error) { // E.g. a corrupt or deleted dashboard - notifications.toasts.addDanger(error.message); + notifications.toasts.addDanger( + dashboardLoadingErrorStrings.getDashboardLoadError(error.message) + ); history.push(DashboardConstants.LANDING_PAGE_PATH); return; } - if (!savedDashboard) return; - - const savedDashboardState = savedObjectToDashboardState({ - savedDashboard, - usageCollection, - showWriteControls, - savedObjectsTagging, - version: initializerContext.env.packageInfo.version, - }); - - const isViewMode = !showWriteControls || Boolean(savedDashboard.id); - savedDashboardState.viewMode = isViewMode ? ViewMode.VIEW : ViewMode.EDIT; - savedDashboardState.filters = cleanFiltersForSerialize(savedDashboardState.filters); - savedDashboardState.query = migrateLegacyQuery( - savedDashboardState.query || queryString.getDefaultQuery() - ); - - return { savedDashboardState, savedDashboard }; }; diff --git a/src/plugins/dashboard/public/dashboard_strings.ts b/src/plugins/dashboard/public/dashboard_strings.ts index ca0f51976f3fb..52961c43cc1a2 100644 --- a/src/plugins/dashboard/public/dashboard_strings.ts +++ b/src/plugins/dashboard/public/dashboard_strings.ts @@ -359,6 +359,14 @@ export const panelStorageErrorStrings = { }), }; +export const dashboardLoadingErrorStrings = { + getDashboardLoadError: (message: string) => + i18n.translate('dashboard.loadingError.errorMessage', { + defaultMessage: 'Error encountered while loading saved dashboard: {message}', + values: { message }, + }), +}; + /* Empty Screen */ diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index dd930887f9d19..a84fddb19c5fa 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -62,6 +62,8 @@ export const getAggTypes = () => ({ { name: BUCKET_TYPES.SIGNIFICANT_TERMS, fn: buckets.getSignificantTermsBucketAgg }, { name: BUCKET_TYPES.GEOHASH_GRID, fn: buckets.getGeoHashBucketAgg }, { name: BUCKET_TYPES.GEOTILE_GRID, fn: buckets.getGeoTitleBucketAgg }, + { name: BUCKET_TYPES.SAMPLER, fn: buckets.getSamplerBucketAgg }, + { name: BUCKET_TYPES.DIVERSIFIED_SAMPLER, fn: buckets.getDiversifiedSamplerBucketAgg }, ], }); @@ -79,6 +81,8 @@ export const getAggTypesFunctions = () => [ buckets.aggDateHistogram, buckets.aggTerms, buckets.aggMultiTerms, + buckets.aggSampler, + buckets.aggDiversifiedSampler, metrics.aggAvg, metrics.aggBucketAvg, metrics.aggBucketMax, diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index be3fbae26174a..571083c18156f 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -73,6 +73,8 @@ describe('Aggs service', () => { "significant_terms", "geohash_grid", "geotile_grid", + "sampler", + "diversified_sampler", "foo", ] `); @@ -122,6 +124,8 @@ describe('Aggs service', () => { "significant_terms", "geohash_grid", "geotile_grid", + "sampler", + "diversified_sampler", ] `); expect(bStart.types.getAll().metrics.map((t) => t(aggTypesDependencies).name)) diff --git a/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts index 0c01bff90bfee..671266ef15997 100644 --- a/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts +++ b/src/plugins/data/common/search/aggs/buckets/bucket_agg_types.ts @@ -19,4 +19,6 @@ export enum BUCKET_TYPES { GEOHASH_GRID = 'geohash_grid', GEOTILE_GRID = 'geotile_grid', DATE_HISTOGRAM = 'date_histogram', + SAMPLER = 'sampler', + DIVERSIFIED_SAMPLER = 'diversified_sampler', } diff --git a/src/plugins/data/common/search/aggs/buckets/diversified_sampler.ts b/src/plugins/data/common/search/aggs/buckets/diversified_sampler.ts new file mode 100644 index 0000000000000..31ebaa094c368 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/diversified_sampler.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { BucketAggType } from './bucket_agg_type'; +import { BaseAggParams } from '../types'; +import { aggDiversifiedSamplerFnName } from './diversified_sampler_fn'; + +export const DIVERSIFIED_SAMPLER_AGG_NAME = 'diversified_sampler'; + +const title = i18n.translate('data.search.aggs.buckets.diversifiedSamplerTitle', { + defaultMessage: 'Diversified sampler', + description: 'Diversified sampler aggregation title', +}); + +export interface AggParamsDiversifiedSampler extends BaseAggParams { + /** + * Is used to provide values used for de-duplication + */ + field: string; + + /** + * Limits how many top-scoring documents are collected in the sample processed on each shard. + */ + shard_size?: number; + + /** + * Limits how many documents are permitted per choice of de-duplicating value + */ + max_docs_per_value?: number; +} + +/** + * Like the sampler aggregation this is a filtering aggregation used to limit any sub aggregations' processing to a sample of the top-scoring documents. + * The diversified_sampler aggregation adds the ability to limit the number of matches that share a common value. + */ +export const getDiversifiedSamplerBucketAgg = () => + new BucketAggType({ + name: DIVERSIFIED_SAMPLER_AGG_NAME, + title, + customLabels: false, + expressionName: aggDiversifiedSamplerFnName, + params: [ + { + name: 'shard_size', + type: 'number', + }, + { + name: 'max_docs_per_value', + type: 'number', + }, + { + name: 'field', + type: 'field', + }, + ], + }); diff --git a/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.test.ts new file mode 100644 index 0000000000000..e874542289bb2 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggDiversifiedSampler } from './diversified_sampler_fn'; + +describe('aggDiversifiedSampler', () => { + const fn = functionWrapper(aggDiversifiedSampler()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ id: 'sampler', schema: 'bucket', field: 'author' }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": "sampler", + "params": Object { + "field": "author", + "max_docs_per_value": undefined, + "shard_size": undefined, + }, + "schema": "bucket", + "type": "diversified_sampler", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: 'sampler', + schema: 'bucket', + shard_size: 300, + field: 'author', + max_docs_per_value: 3, + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": "sampler", + "params": Object { + "field": "author", + "max_docs_per_value": 3, + "shard_size": 300, + }, + "schema": "bucket", + "type": "diversified_sampler", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.ts b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.ts new file mode 100644 index 0000000000000..0e1b235dd576d --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/diversified_sampler_fn.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionFunctionArgs, AggExpressionType, BUCKET_TYPES } from '../'; +import { DIVERSIFIED_SAMPLER_AGG_NAME } from './diversified_sampler'; + +export const aggDiversifiedSamplerFnName = 'aggDiversifiedSampler'; + +type Input = any; +type Arguments = AggExpressionFunctionArgs; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggDiversifiedSamplerFnName, + Input, + Arguments, + Output +>; + +export const aggDiversifiedSampler = (): FunctionDefinition => ({ + name: aggDiversifiedSamplerFnName, + help: i18n.translate('data.search.aggs.function.buckets.diversifiedSampler.help', { + defaultMessage: 'Generates a serialized agg config for a Diversified sampler agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + shard_size: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.shardSize.help', { + defaultMessage: + 'The shard_size parameter limits how many top-scoring documents are collected in the sample processed on each shard.', + }), + }, + max_docs_per_value: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.maxDocsPerValue.help', { + defaultMessage: + 'Limits how many documents are permitted per choice of de-duplicating value.', + }), + }, + field: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.diversifiedSampler.field.help', { + defaultMessage: 'Used to provide values used for de-duplication.', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: DIVERSIFIED_SAMPLER_AGG_NAME, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/buckets/index.ts b/src/plugins/data/common/search/aggs/buckets/index.ts index 421fa0fcfdaf4..bf96a9ef860c0 100644 --- a/src/plugins/data/common/search/aggs/buckets/index.ts +++ b/src/plugins/data/common/search/aggs/buckets/index.ts @@ -38,3 +38,7 @@ export * from './terms_fn'; export * from './terms'; export * from './multi_terms_fn'; export * from './multi_terms'; +export * from './sampler_fn'; +export * from './sampler'; +export * from './diversified_sampler_fn'; +export * from './diversified_sampler'; diff --git a/src/plugins/data/common/search/aggs/buckets/sampler.ts b/src/plugins/data/common/search/aggs/buckets/sampler.ts new file mode 100644 index 0000000000000..7eb4f74115095 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/sampler.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { BucketAggType } from './bucket_agg_type'; +import { BaseAggParams } from '../types'; +import { aggSamplerFnName } from './sampler_fn'; + +export const SAMPLER_AGG_NAME = 'sampler'; + +const title = i18n.translate('data.search.aggs.buckets.samplerTitle', { + defaultMessage: 'Sampler', + description: 'Sampler aggregation title', +}); + +export interface AggParamsSampler extends BaseAggParams { + /** + * Limits how many top-scoring documents are collected in the sample processed on each shard. + */ + shard_size?: number; +} + +/** + * A filtering aggregation used to limit any sub aggregations' processing to a sample of the top-scoring documents. + */ +export const getSamplerBucketAgg = () => + new BucketAggType({ + name: SAMPLER_AGG_NAME, + title, + customLabels: false, + expressionName: aggSamplerFnName, + params: [ + { + name: 'shard_size', + type: 'number', + }, + ], + }); diff --git a/src/plugins/data/common/search/aggs/buckets/sampler_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/sampler_fn.test.ts new file mode 100644 index 0000000000000..76ef901671e72 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/sampler_fn.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { functionWrapper } from '../test_helpers'; +import { aggSampler } from './sampler_fn'; + +describe('aggSampler', () => { + const fn = functionWrapper(aggSampler()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ id: 'sampler', schema: 'bucket' }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": "sampler", + "params": Object { + "shard_size": undefined, + }, + "schema": "bucket", + "type": "sampler", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: 'sampler', + schema: 'bucket', + shard_size: 300, + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": true, + "id": "sampler", + "params": Object { + "shard_size": 300, + }, + "schema": "bucket", + "type": "sampler", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/aggs/buckets/sampler_fn.ts b/src/plugins/data/common/search/aggs/buckets/sampler_fn.ts new file mode 100644 index 0000000000000..2cb30eb70a230 --- /dev/null +++ b/src/plugins/data/common/search/aggs/buckets/sampler_fn.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionFunctionArgs, AggExpressionType, BUCKET_TYPES } from '../'; +import { SAMPLER_AGG_NAME } from './sampler'; + +export const aggSamplerFnName = 'aggSampler'; + +type Input = any; +type Arguments = AggExpressionFunctionArgs; + +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggSamplerFnName, + Input, + Arguments, + Output +>; + +export const aggSampler = (): FunctionDefinition => ({ + name: aggSamplerFnName, + help: i18n.translate('data.search.aggs.function.buckets.sampler.help', { + defaultMessage: 'Generates a serialized agg config for a Sampler agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.sampler.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.sampler.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.sampler.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + shard_size: { + types: ['number'], + help: i18n.translate('data.search.aggs.buckets.sampler.shardSize.help', { + defaultMessage: + 'The shard_size parameter limits how many top-scoring documents are collected in the sample processed on each shard.', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: SAMPLER_AGG_NAME, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index b9a977e0a8a09..9c4866c19714d 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -90,6 +90,8 @@ import { aggFilteredMetric, aggSinglePercentile, } from './'; +import { AggParamsSampler } from './buckets/sampler'; +import { AggParamsDiversifiedSampler } from './buckets/diversified_sampler'; export type { IAggConfig, AggConfigSerialized } from './agg_config'; export type { CreateAggConfigParams, IAggConfigs } from './agg_configs'; @@ -166,6 +168,8 @@ export interface AggParamsMapping { [BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram; [BUCKET_TYPES.TERMS]: AggParamsTerms; [BUCKET_TYPES.MULTI_TERMS]: AggParamsMultiTerms; + [BUCKET_TYPES.SAMPLER]: AggParamsSampler; + [BUCKET_TYPES.DIVERSIFIED_SAMPLER]: AggParamsDiversifiedSampler; [METRIC_TYPES.AVG]: AggParamsAvg; [METRIC_TYPES.CARDINALITY]: AggParamsCardinality; [METRIC_TYPES.COUNT]: BaseAggParams; diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 20e07360a68e5..c7df4354cc76b 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -53,7 +53,7 @@ describe('AggsService - public', () => { test('registers default agg types', () => { service.setup(setupDeps); const start = service.start(startDeps); - expect(start.types.getAll().buckets.length).toBe(12); + expect(start.types.getAll().buckets.length).toBe(14); expect(start.types.getAll().metrics.length).toBe(23); }); @@ -69,7 +69,7 @@ describe('AggsService - public', () => { ); const start = service.start(startDeps); - expect(start.types.getAll().buckets.length).toBe(13); + expect(start.types.getAll().buckets.length).toBe(15); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); expect(start.types.getAll().metrics.length).toBe(24); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); diff --git a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx index 602db0cd55274..c5eaeb02c05cf 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -218,6 +218,7 @@ const ScriptFieldComponent = ({ existingConcreteFields, links }: Props) => { <> { - private readonly tutorialsRegistry = new TutorialsRegistry(); + private readonly tutorialsRegistry; private readonly sampleDataRegistry: SampleDataRegistry; private customIntegrations?: CustomIntegrationsPluginSetup; constructor(private readonly initContext: PluginInitializerContext) { this.sampleDataRegistry = new SampleDataRegistry(this.initContext); + this.tutorialsRegistry = new TutorialsRegistry(this.initContext); } public setup(core: CoreSetup, plugins: HomeServerPluginSetupDependencies): HomeServerPluginSetup { diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index 4c80c8858a475..aeebecf6cab32 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -29,6 +29,7 @@ export enum TutorialsCategory { export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM'; export interface TutorialContext { + kibanaBranch: string; [key: string]: unknown; } export type TutorialProvider = (context: TutorialContext) => TutorialSchema; diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index ee73c8e13f62b..dec1d23e05787 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -69,6 +69,7 @@ const validTutorialProvider = VALID_TUTORIAL; describe('TutorialsRegistry', () => { let mockCoreSetup: MockedKeys; + let mockInitContext: ReturnType; let testProvider: TutorialProvider; let testScopedTutorialContextFactory: ScopedTutorialContextFactory; let mockCustomIntegrationsPluginSetup: jest.Mocked; @@ -80,6 +81,7 @@ describe('TutorialsRegistry', () => { describe('GET /api/kibana/home/tutorials', () => { beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockInitContext = coreMock.createPluginInitializerContext(); }); test('has a router that retrieves registered tutorials', () => { @@ -90,13 +92,19 @@ describe('TutorialsRegistry', () => { describe('setup', () => { test('exposes proper contract', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); expect(setup).toHaveProperty('registerTutorial'); expect(setup).toHaveProperty('addScopedTutorialContextFactory'); }); test('registerTutorial throws when registering a tutorial with an invalid schema', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); testProvider = ({}) => invalidTutorialProvider; expect(() => setup.registerTutorial(testProvider)).toThrowErrorMatchingInlineSnapshot( `"Unable to register tutorial spec because its invalid. Error: [name]: is not allowed to be empty"` @@ -104,7 +112,10 @@ describe('TutorialsRegistry', () => { }); test('registerTutorial registers a tutorial with a valid schema', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); testProvider = ({}) => validTutorialProvider; expect(() => setup.registerTutorial(testProvider)).not.toThrowError(); expect(mockCustomIntegrationsPluginSetup.registerCustomIntegration.mock.calls).toEqual([ @@ -129,7 +140,10 @@ describe('TutorialsRegistry', () => { }); test('addScopedTutorialContextFactory throws when given a scopedTutorialContextFactory that is not a function', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); const testItem = {} as TutorialProvider; expect(() => setup.addScopedTutorialContextFactory(testItem) @@ -139,7 +153,10 @@ describe('TutorialsRegistry', () => { }); test('addScopedTutorialContextFactory adds a scopedTutorialContextFactory when given a function', () => { - const setup = new TutorialsRegistry().setup(mockCoreSetup, mockCustomIntegrationsPluginSetup); + const setup = new TutorialsRegistry(mockInitContext).setup( + mockCoreSetup, + mockCustomIntegrationsPluginSetup + ); testScopedTutorialContextFactory = ({}) => 'string'; expect(() => setup.addScopedTutorialContextFactory(testScopedTutorialContextFactory) @@ -149,7 +166,7 @@ describe('TutorialsRegistry', () => { describe('start', () => { test('exposes proper contract', () => { - const start = new TutorialsRegistry().start( + const start = new TutorialsRegistry(mockInitContext).start( coreMock.createStart(), mockCustomIntegrationsPluginSetup ); diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.ts index 723c92e6dfaf4..7d93a57b2073d 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import { CoreSetup, CoreStart } from 'src/core/server'; +import { CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/server'; import { TutorialProvider, TutorialContextFactory, ScopedTutorialContextFactory, + TutorialContext, } from './lib/tutorials_registry_types'; import { TutorialSchema, tutorialSchema } from './lib/tutorial_schema'; import { builtInTutorials } from '../../tutorials/register'; @@ -71,12 +72,14 @@ export class TutorialsRegistry { private tutorialProviders: TutorialProvider[] = []; // pre-register all the tutorials we know we want in here private readonly scopedTutorialContextFactories: TutorialContextFactory[] = []; + constructor(private readonly initContext: PluginInitializerContext) {} + public setup(core: CoreSetup, customIntegrations?: CustomIntegrationsPluginSetup) { const router = core.http.createRouter(); router.get( { path: '/api/kibana/home/tutorials', validate: false }, async (context, req, res) => { - const initialContext = {}; + const initialContext = this.baseTutorialContext; const scopedContext = this.scopedTutorialContextFactories.reduce( (accumulatedContext, contextFactory) => { return { ...accumulatedContext, ...contextFactory(req) }; @@ -92,7 +95,7 @@ export class TutorialsRegistry { ); return { registerTutorial: (specProvider: TutorialProvider) => { - const emptyContext = {}; + const emptyContext = this.baseTutorialContext; let tutorial: TutorialSchema; try { tutorial = tutorialSchema.validate(specProvider(emptyContext)); @@ -132,12 +135,16 @@ export class TutorialsRegistry { if (customIntegrations) { builtInTutorials.forEach((provider) => { - const tutorial = provider({}); + const tutorial = provider(this.baseTutorialContext); registerBeatsTutorialsWithCustomIntegrations(core, customIntegrations, tutorial); }); } return {}; } + + private get baseTutorialContext(): TutorialContext { + return { kibanaBranch: this.initContext.env.packageInfo.branch }; + } } /** @public */ diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index a277b37838562..cc84f9a536b22 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -56,8 +56,8 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/activemq_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts index 9a001c149cda0..9c98c9c2ffc7a 100644 --- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts @@ -54,8 +54,8 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; diff --git a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts index 3e574f2c75496..1cc350af579cb 100644 --- a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts @@ -54,8 +54,8 @@ export function aerospikeMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts index 6e588fd86588d..aea8e3c188d94 100644 --- a/src/plugins/home/server/tutorials/apache_logs/index.ts +++ b/src/plugins/home/server/tutorials/apache_logs/index.ts @@ -57,8 +57,8 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/apache_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts index 17b495d1460c5..0af719610c24d 100644 --- a/src/plugins/home/server/tutorials/apache_metrics/index.ts +++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts @@ -56,8 +56,8 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/apache_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts index 96e5d4bcda393..666fcf15635c3 100644 --- a/src/plugins/home/server/tutorials/auditbeat/index.ts +++ b/src/plugins/home/server/tutorials/auditbeat/index.ts @@ -56,8 +56,8 @@ processes, users, logins, sockets information, file accesses, and more. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/auditbeat/screenshot.png', onPrem: onPremInstructions(platforms, context), - elasticCloud: cloudInstructions(platforms), - onPremElasticCloud: onPremCloudInstructions(platforms), + elasticCloud: cloudInstructions(platforms, context), + onPremElasticCloud: onPremCloudInstructions(platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/auditd_logs/index.ts b/src/plugins/home/server/tutorials/auditd_logs/index.ts index 6993196d93417..24857045ccc28 100644 --- a/src/plugins/home/server/tutorials/auditd_logs/index.ts +++ b/src/plugins/home/server/tutorials/auditd_logs/index.ts @@ -57,8 +57,8 @@ export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/auditd_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['os_system'], }; } diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts index 62fbcc4eebc18..60187490318ae 100644 --- a/src/plugins/home/server/tutorials/aws_logs/index.ts +++ b/src/plugins/home/server/tutorials/aws_logs/index.ts @@ -57,8 +57,8 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/aws_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts index 6bf1bf64bff9f..6541b4f5f29c8 100644 --- a/src/plugins/home/server/tutorials/aws_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts @@ -58,8 +58,8 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/aws_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } diff --git a/src/plugins/home/server/tutorials/azure_logs/index.ts b/src/plugins/home/server/tutorials/azure_logs/index.ts index 3c9438d9a6298..163496813567a 100644 --- a/src/plugins/home/server/tutorials/azure_logs/index.ts +++ b/src/plugins/home/server/tutorials/azure_logs/index.ts @@ -58,8 +58,8 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/azure_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['azure', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/azure_metrics/index.ts b/src/plugins/home/server/tutorials/azure_metrics/index.ts index 310f954104634..edf4062812b42 100644 --- a/src/plugins/home/server/tutorials/azure_metrics/index.ts +++ b/src/plugins/home/server/tutorials/azure_metrics/index.ts @@ -57,8 +57,8 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/azure_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['azure', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/barracuda_logs/index.ts b/src/plugins/home/server/tutorials/barracuda_logs/index.ts index cdfd75b9728b9..7cf333ec6f7e5 100644 --- a/src/plugins/home/server/tutorials/barracuda_logs/index.ts +++ b/src/plugins/home/server/tutorials/barracuda_logs/index.ts @@ -55,8 +55,8 @@ export function barracudaLogsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts index a7db5b04ee40d..f35cd0ac4e450 100644 --- a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts +++ b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts @@ -54,8 +54,8 @@ export function bluecoatLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/cef_logs/index.ts b/src/plugins/home/server/tutorials/cef_logs/index.ts index 1366198d610d7..bf1f402a09a65 100644 --- a/src/plugins/home/server/tutorials/cef_logs/index.ts +++ b/src/plugins/home/server/tutorials/cef_logs/index.ts @@ -61,8 +61,8 @@ export function cefLogsSpecProvider(context: TutorialContext): TutorialSchema { }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/ceph_metrics/index.ts b/src/plugins/home/server/tutorials/ceph_metrics/index.ts index 6a53789d26f7c..e7d2c67ec2a99 100644 --- a/src/plugins/home/server/tutorials/ceph_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ceph_metrics/index.ts @@ -54,8 +54,8 @@ export function cephMetricsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts index b5ea6be42403b..83ce8d27ec861 100644 --- a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts +++ b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts @@ -54,8 +54,8 @@ export function checkpointLogsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index 922cfbf1e23ee..3c855996873af 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -57,8 +57,8 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/cisco_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index 5564d11be4d19..a4172fae4ff4d 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -51,8 +51,8 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions([], context), - elasticCloud: cloudInstructions(), - onPremElasticCloud: onPremCloudInstructions(), + elasticCloud: cloudInstructions(context), + onPremElasticCloud: onPremCloudInstructions(context), integrationBrowserCategories: ['aws', 'cloud', 'datastore', 'security', 'network'], }; } diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts index 535c8aaa90768..d53fd7f1f73aa 100644 --- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts @@ -59,8 +59,8 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/cockroachdb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts index ca7179d55fd89..26fff9e58f511 100644 --- a/src/plugins/home/server/tutorials/consul_metrics/index.ts +++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts @@ -56,8 +56,8 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/consul_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index 1261c67135001..876e6e09d61d6 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -57,8 +57,8 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/coredns_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts index 3abc14314a6ba..b854f4d448361 100644 --- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts +++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts @@ -54,8 +54,8 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/coredns_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts index 5c29aa2d9a524..2a71a6d0457f1 100644 --- a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts @@ -54,8 +54,8 @@ export function couchbaseMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts index 00bea11d13d99..a379b3b04f4c7 100644 --- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts @@ -59,8 +59,8 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/couchdb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security', 'network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts index a48ed4288210b..2c5a32b63f75f 100644 --- a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts +++ b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts @@ -58,8 +58,8 @@ export function crowdstrikeLogsSpecProvider(context: TutorialContext): TutorialS }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/cylance_logs/index.ts b/src/plugins/home/server/tutorials/cylance_logs/index.ts index 64b79a41cd2e0..d8b72963678fa 100644 --- a/src/plugins/home/server/tutorials/cylance_logs/index.ts +++ b/src/plugins/home/server/tutorials/cylance_logs/index.ts @@ -54,8 +54,8 @@ export function cylanceLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts index ab80e6d644dbc..e36d590650454 100644 --- a/src/plugins/home/server/tutorials/docker_metrics/index.ts +++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts @@ -56,8 +56,8 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/docker_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['containers', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts index 9864d376966bb..f01119e6ba1d2 100644 --- a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts +++ b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts @@ -54,8 +54,8 @@ export function dropwizardMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts index 6415781d02c06..a1df2d8a4085e 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts @@ -56,8 +56,8 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/elasticsearch_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['containers', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts index 3961d7f78c86c..009e441c725d9 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts @@ -54,8 +54,8 @@ export function elasticsearchMetricsSpecProvider(context: TutorialContext): Tuto }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index 55c85a5bdd2a4..d39b182b81eaf 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -60,8 +60,8 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/envoyproxy_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts index e2f3b84739685..84ea8099e3d93 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts @@ -47,8 +47,8 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/etcd_metrics/index.ts b/src/plugins/home/server/tutorials/etcd_metrics/index.ts index 9ed153c21c257..c4c68e80d40eb 100644 --- a/src/plugins/home/server/tutorials/etcd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/etcd_metrics/index.ts @@ -54,8 +54,8 @@ export function etcdMetricsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['elastic_stack', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/f5_logs/index.ts b/src/plugins/home/server/tutorials/f5_logs/index.ts index a407d1d3d5142..381fdd487eb24 100644 --- a/src/plugins/home/server/tutorials/f5_logs/index.ts +++ b/src/plugins/home/server/tutorials/f5_logs/index.ts @@ -55,8 +55,8 @@ export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/f5_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/fortinet_logs/index.ts b/src/plugins/home/server/tutorials/fortinet_logs/index.ts index 2f6af3ba47280..6a73c5f8e3f66 100644 --- a/src/plugins/home/server/tutorials/fortinet_logs/index.ts +++ b/src/plugins/home/server/tutorials/fortinet_logs/index.ts @@ -54,8 +54,8 @@ export function fortinetLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/gcp_logs/index.ts b/src/plugins/home/server/tutorials/gcp_logs/index.ts index 23d8e3364eb69..d02c08cd2be9a 100644 --- a/src/plugins/home/server/tutorials/gcp_logs/index.ts +++ b/src/plugins/home/server/tutorials/gcp_logs/index.ts @@ -59,8 +59,8 @@ export function gcpLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/gcp_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['google_cloud', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/gcp_metrics/index.ts b/src/plugins/home/server/tutorials/gcp_metrics/index.ts index 7f397c1e1be7b..ea5351d010a42 100644 --- a/src/plugins/home/server/tutorials/gcp_metrics/index.ts +++ b/src/plugins/home/server/tutorials/gcp_metrics/index.ts @@ -57,8 +57,8 @@ export function gcpMetricsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/gcp_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['google_cloud', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/golang_metrics/index.ts b/src/plugins/home/server/tutorials/golang_metrics/index.ts index 50d09e42e8791..e179e69734ad5 100644 --- a/src/plugins/home/server/tutorials/golang_metrics/index.ts +++ b/src/plugins/home/server/tutorials/golang_metrics/index.ts @@ -57,8 +57,8 @@ export function golangMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['google_cloud', 'cloud', 'network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/gsuite_logs/index.ts b/src/plugins/home/server/tutorials/gsuite_logs/index.ts index 718558321cf78..ba193bdb08c08 100644 --- a/src/plugins/home/server/tutorials/gsuite_logs/index.ts +++ b/src/plugins/home/server/tutorials/gsuite_logs/index.ts @@ -54,8 +54,8 @@ export function gsuiteLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/haproxy_logs/index.ts b/src/plugins/home/server/tutorials/haproxy_logs/index.ts index c3765317ecbe0..05fc23fa16bcd 100644 --- a/src/plugins/home/server/tutorials/haproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_logs/index.ts @@ -57,8 +57,8 @@ export function haproxyLogsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/haproxy_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts index 49f1d32dc4c82..fa7c451889ba3 100644 --- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts @@ -54,8 +54,8 @@ export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['network', 'web'], }; } diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts index 21b60a9ab5a5c..90b35d0e78842 100644 --- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts @@ -56,8 +56,8 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/ibmmq_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts index 706003f0eab48..6329df6836b06 100644 --- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts @@ -55,8 +55,8 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/ibmmq_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/icinga_logs/index.ts b/src/plugins/home/server/tutorials/icinga_logs/index.ts index dc730022262c2..c65e92d0fe856 100644 --- a/src/plugins/home/server/tutorials/icinga_logs/index.ts +++ b/src/plugins/home/server/tutorials/icinga_logs/index.ts @@ -57,8 +57,8 @@ export function icingaLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/icinga_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts index 0dbc5bbdc75b8..423f2f917c84e 100644 --- a/src/plugins/home/server/tutorials/iis_logs/index.ts +++ b/src/plugins/home/server/tutorials/iis_logs/index.ts @@ -58,8 +58,8 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/iis_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/iis_metrics/index.ts b/src/plugins/home/server/tutorials/iis_metrics/index.ts index d57e4688ba753..3c3159c2838d1 100644 --- a/src/plugins/home/server/tutorials/iis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/iis_metrics/index.ts @@ -57,8 +57,8 @@ export function iisMetricsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/iis_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web'], }; } diff --git a/src/plugins/home/server/tutorials/imperva_logs/index.ts b/src/plugins/home/server/tutorials/imperva_logs/index.ts index 1cbe707f813ee..35e0a668ec7f0 100644 --- a/src/plugins/home/server/tutorials/imperva_logs/index.ts +++ b/src/plugins/home/server/tutorials/imperva_logs/index.ts @@ -54,8 +54,8 @@ export function impervaLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/infoblox_logs/index.ts b/src/plugins/home/server/tutorials/infoblox_logs/index.ts index 8dce2bf00b2e2..21d1fcf9a156c 100644 --- a/src/plugins/home/server/tutorials/infoblox_logs/index.ts +++ b/src/plugins/home/server/tutorials/infoblox_logs/index.ts @@ -54,8 +54,8 @@ export function infobloxLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network'], }; } diff --git a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts index d0a0f97e26037..3968aff312380 100644 --- a/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/auditbeat_instructions.ts @@ -13,271 +13,317 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createAuditbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd auditbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i auditbeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi auditbeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Auditbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.auditbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Auditbeat? See the [Quick Start]({guideLinkUrl}).\n\ +export const createAuditbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/auditbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf auditbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd auditbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i auditbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Auditbeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi auditbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.rpmTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Auditbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Auditbeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Auditbeat Windows zip file from the [Download]({auditbeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Auditbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Auditbeat as a Windows service.', + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: + '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', + auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', + directoryName: 'auditbeat-{config.kibana.version}-windows', + }, + } + ), + commands: ['cd "C:\\Program Files\\Auditbeat"', '.\\install-service-auditbeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under {propertyName} in the {auditbeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + auditbeatPath: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + }, + } + ), + }, + }, + START: { + OSX: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['./auditbeat setup', './auditbeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Auditbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.start.windowsTextPre', + { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + } + ), + commands: ['.\\auditbeat.exe setup', 'Start-Service auditbeat'], + }, + }, + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.auditbeat}/auditbeat-installation-configuration.html', - auditbeatLinkUrl: 'https://www.elastic.co/downloads/beats/auditbeat', - directoryName: 'auditbeat-{config.kibana.version}-windows', + path: '`auditbeat.yml`', }, - } - ), - commands: ['cd "C:\\Program Files\\Auditbeat"', '.\\install-service-auditbeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatInstructions.install.windowsTextPost', - { - defaultMessage: - 'Modify the settings under {propertyName} in the {auditbeatPath} file to point to your Elasticsearch installation.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - propertyName: '`output.elasticsearch`', - auditbeatPath: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + path: '`/etc/auditbeat/auditbeat.yml`', }, - } - ), - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.osxTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['./auditbeat setup', './auditbeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.debTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.rpmTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo auditbeat setup', 'sudo service auditbeat start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Auditbeat', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\auditbeat.exe setup', 'Start-Service auditbeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/auditbeat/auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/auditbeat/auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.windowsTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.auditbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.auditbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`/etc/auditbeat/auditbeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.auditbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Auditbeat\\auditbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.auditbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createAuditbeatCloudInstructions = () => ({ CONFIG: { @@ -383,7 +429,7 @@ export function auditbeatStatusCheck() { }; } -export function onPremInstructions(platforms: readonly Platform[], context?: TutorialContext) { +export function onPremInstructions(platforms: readonly Platform[], context: TutorialContext) { const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); const variants = []; @@ -414,8 +460,8 @@ export function onPremInstructions(platforms: readonly Platform[], context?: Tut }; } -export function onPremCloudInstructions(platforms: readonly Platform[]) { - const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(); +export function onPremCloudInstructions(platforms: readonly Platform[], context: TutorialContext) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); @@ -450,8 +496,8 @@ export function onPremCloudInstructions(platforms: readonly Platform[]) { }; } -export function cloudInstructions(platforms: readonly Platform[]) { - const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(); +export function cloudInstructions(platforms: readonly Platform[], context: TutorialContext) { + const AUDITBEAT_INSTRUCTIONS = createAuditbeatInstructions(context); const AUDITBEAT_CLOUD_INSTRUCTIONS = createAuditbeatCloudInstructions(); const variants = []; diff --git a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts index c6aa44932ee45..89445510f2b3d 100644 --- a/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/filebeat_instructions.ts @@ -13,268 +13,307 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createFilebeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd filebeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', - values: { - linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', - values: { - linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTextPre', { - defaultMessage: - 'First time using Filebeat? See the [Quick Start]({guideLinkUrl}).\n\ +export const createFilebeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/filebeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd filebeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), + }, + RPM: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Filebeat? See the [Quick Start]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate('home.tutorials.common.filebeatInstructions.install.rpmTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.filebeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Filebeat? See the [Quick Start]({guideLinkUrl}).\n\ 1. Download the Filebeat Windows zip file from the [Download]({filebeatLinkUrl}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the `{directoryName}` directory to `Filebeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', - values: { - folderPath: '`C:\\Program Files`', - guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', - filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', - directoryName: 'filebeat-{config.kibana.version}-windows', - }, - }), - commands: ['cd "C:\\Program Files\\Filebeat"', '.\\install-service-filebeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.filebeatInstructions.install.windowsTextPost', - { + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-installation-configuration.html', + filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + directoryName: 'filebeat-{config.kibana.version}-windows', + }, + } + ), + commands: ['cd "C:\\Program Files\\Filebeat"', '.\\install-service-filebeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under {propertyName} in the {filebeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + filebeatPath: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + } + ), + }, + }, + START: { + OSX: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['./filebeat setup', './filebeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo filebeat setup', 'sudo service filebeat start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTextPre', { defaultMessage: - 'Modify the settings under {propertyName} in the {filebeatPath} file to point to your Elasticsearch installation.', + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo filebeat setup', 'sudo service filebeat start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['.\\filebeat.exe setup', 'Start-Service filebeat'], + }, + }, + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - propertyName: '`output.elasticsearch`', - filebeatPath: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + path: '`filebeat.yml`', }, - } - ), - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.osxTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['./filebeat setup', './filebeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.debTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo filebeat setup', 'sudo service filebeat start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.rpmTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo filebeat setup', 'sudo service filebeat start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Filebeat', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\filebeat.exe setup', 'Start-Service filebeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/filebeat/filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/filebeat/filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.filebeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.windowsTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.filebeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`/etc/filebeat/filebeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.filebeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.filebeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.filebeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createFilebeatCloudInstructions = () => ({ CONFIG: { @@ -430,7 +469,7 @@ export function filebeatStatusCheck(moduleName: string) { export function onPremInstructions( moduleName: string, platforms: readonly Platform[] = [], - context?: TutorialContext + context: TutorialContext ) { const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(context); @@ -463,8 +502,12 @@ export function onPremInstructions( }; } -export function onPremCloudInstructions(moduleName: string, platforms: readonly Platform[] = []) { - const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); +export function onPremCloudInstructions( + moduleName: string, + platforms: readonly Platform[] = [], + context: TutorialContext +) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(context); const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); @@ -500,8 +543,12 @@ export function onPremCloudInstructions(moduleName: string, platforms: readonly }; } -export function cloudInstructions(moduleName: string, platforms: readonly Platform[] = []) { - const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(); +export function cloudInstructions( + moduleName: string, + platforms: readonly Platform[] = [], + context: TutorialContext +) { + const FILEBEAT_INSTRUCTIONS = createFilebeatInstructions(context); const FILEBEAT_CLOUD_INSTRUCTIONS = createFilebeatCloudInstructions(); const variants = []; diff --git a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts index 24a6fe3719f8f..60d6fa5cb813b 100644 --- a/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/functionbeat_instructions.ts @@ -13,171 +13,203 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createFunctionbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Functionbeat', - }), - textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd functionbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - LINUX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.linuxTitle', { - defaultMessage: 'Download and install Functionbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.install.linuxTextPre', - { - defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', - }, - } - ), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', - 'tar xzvf functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', - 'cd functionbeat-{config.kibana.version}-linux-x86_64/', - ], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Functionbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Functionbeat? See the [Quick Start]({functionbeatLink}).\n\ +export const createFunctionbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/functionbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Functionbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.osxTextPre', + { + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, + } + ), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf functionbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd functionbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + LINUX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.install.linuxTitle', { + defaultMessage: 'Download and install Functionbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.linuxTextPre', + { + defaultMessage: 'First time using Functionbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + }, + } + ), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/functionbeat/functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', + 'tar xzvf functionbeat-{config.kibana.version}-linux-x86_64.tar.gz', + 'cd functionbeat-{config.kibana.version}-linux-x86_64/', + ], + }, + WINDOWS: { + title: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.windowsTitle', + { + defaultMessage: 'Download and install Functionbeat', + } + ), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Functionbeat? See the [Quick Start]({functionbeatLink}).\n\ 1. Download the Functionbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Functionbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, go to the Functionbeat directory:', - values: { - directoryName: '`functionbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - functionbeatLink: - '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/functionbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Functionbeat"'], + values: { + directoryName: '`functionbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + functionbeatLink: + '{config.docs.beats.functionbeat}/functionbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/functionbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Functionbeat"'], + }, }, - }, - DEPLOY: { - OSX_LINUX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.osxTitle', { - defaultMessage: 'Deploy Functionbeat to AWS Lambda', - }), - textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.osxTextPre', { - defaultMessage: - 'This installs Functionbeat as a Lambda function.\ + DEPLOY: { + OSX_LINUX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.osxTitle', { + defaultMessage: 'Deploy Functionbeat to AWS Lambda', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.deploy.osxTextPre', + { + defaultMessage: + 'This installs Functionbeat as a Lambda function.\ The `setup` command checks the Elasticsearch configuration and loads the \ Kibana index pattern. It is normally safe to omit this command.', - }), - commands: ['./functionbeat setup', './functionbeat deploy fn-cloudwatch-logs'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.deploy.windowsTitle', { - defaultMessage: 'Deploy Functionbeat to AWS Lambda', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre', - { - defaultMessage: - 'This installs Functionbeat as a Lambda function.\ + } + ), + commands: ['./functionbeat setup', './functionbeat deploy fn-cloudwatch-logs'], + }, + WINDOWS: { + title: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.deploy.windowsTitle', + { + defaultMessage: 'Deploy Functionbeat to AWS Lambda', + } + ), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.deploy.windowsTextPre', + { + defaultMessage: + 'This installs Functionbeat as a Lambda function.\ The `setup` command checks the Elasticsearch configuration and loads the \ Kibana index pattern. It is normally safe to omit this command.', - } - ), - commands: ['.\\functionbeat.exe setup', '.\\functionbeat.exe deploy fn-cloudwatch-logs'], - }, - }, - CONFIG: { - OSX_LINUX: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.config.osxTitle', { - defaultMessage: 'Configure the Elastic cluster', - }), - textPre: i18n.translate('home.tutorials.common.functionbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`functionbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.config.osxTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - } - ), + } + ), + commands: ['.\\functionbeat.exe setup', '.\\functionbeat.exe deploy fn-cloudwatch-logs'], + }, }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.functionbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.config.windowsTextPre', - { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Functionbeat\\functionbeat.yml`', - }, - } - ), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.functionbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - } - ), + CONFIG: { + OSX_LINUX: { + title: i18n.translate('home.tutorials.common.functionbeatInstructions.config.osxTitle', { + defaultMessage: 'Configure the Elastic cluster', + }), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.osxTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`functionbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.windowsTitle', + { + defaultMessage: 'Edit the configuration', + } + ), + textPre: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Functionbeat\\functionbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.functionbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createFunctionbeatCloudInstructions = () => ({ CONFIG: { @@ -336,7 +368,7 @@ export function functionbeatStatusCheck() { }; } -export function onPremInstructions(platforms: Platform[], context?: TutorialContext) { +export function onPremInstructions(platforms: Platform[], context: TutorialContext) { const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(context); return { @@ -386,10 +418,10 @@ export function onPremInstructions(platforms: Platform[], context?: TutorialCont }; } -export function onPremCloudInstructions() { +export function onPremCloudInstructions(context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(); + const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(context); return { instructionSets: [ @@ -444,8 +476,8 @@ export function onPremCloudInstructions() { }; } -export function cloudInstructions() { - const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(); +export function cloudInstructions(context: TutorialContext) { + const FUNCTIONBEAT_INSTRUCTIONS = createFunctionbeatInstructions(context); const FUNCTIONBEAT_CLOUD_INSTRUCTIONS = createFunctionbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts index ce3e76a5f827e..5cbd1641bf09a 100644 --- a/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/heartbeat_instructions.ts @@ -13,247 +13,298 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { Platform, TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createHeartbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd heartbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i heartbeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', - values: { link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html' }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi heartbeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Heartbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.heartbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Heartbeat? See the [Quick Start]({heartbeatLink}).\n\ +export const createHeartbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/heartbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf heartbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd heartbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i heartbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Heartbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/heartbeat/heartbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi heartbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/heartbeat' }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Heartbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Heartbeat? See the [Quick Start]({heartbeatLink}).\n\ 1. Download the Heartbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Heartbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Heartbeat as a Windows service.', - values: { - directoryName: '`heartbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - heartbeatLink: - '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/heartbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Heartbeat"', '.\\install-service-heartbeat.ps1'], - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['./heartbeat setup', './heartbeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Heartbeat', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.windowsTextPre', { - defaultMessage: 'The `setup` command loads the Kibana index pattern.', - }), - commands: ['.\\heartbeat.exe setup', 'Start-Service heartbeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/heartbeat/heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + values: { + directoryName: '`heartbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + heartbeatLink: + '{config.docs.beats.heartbeat}/heartbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/heartbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Heartbeat"', '.\\install-service-heartbeat.ps1'], + }, }, - RPM: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/heartbeat/heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + START: { + OSX: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + }), + commands: ['./heartbeat setup', './heartbeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + }), + commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + }), + commands: ['sudo heartbeat setup', 'sudo service heartbeat-elastic start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Heartbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.start.windowsTextPre', + { + defaultMessage: 'The `setup` command loads the Kibana index pattern.', + } + ), + commands: ['.\\heartbeat.exe setup', 'Start-Service heartbeat'], + }, }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.windowsTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Heartbeat\\heartbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.heartbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`heartbeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/heartbeat/heartbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.heartbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/heartbeat/heartbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.heartbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Heartbeat\\heartbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.heartbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createHeartbeatCloudInstructions = () => ({ CONFIG: { @@ -486,7 +537,7 @@ export function heartbeatStatusCheck() { }; } -export function onPremInstructions(platforms: Platform[], context?: TutorialContext) { +export function onPremInstructions(platforms: Platform[], context: TutorialContext) { const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(context); return { @@ -542,10 +593,10 @@ export function onPremInstructions(platforms: Platform[], context?: TutorialCont }; } -export function onPremCloudInstructions() { +export function onPremCloudInstructions(context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(); + const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(context); return { instructionSets: [ @@ -608,8 +659,8 @@ export function onPremCloudInstructions() { }; } -export function cloudInstructions() { - const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(); +export function cloudInstructions(context: TutorialContext) { + const HEARTBEAT_INSTRUCTIONS = createHeartbeatInstructions(context); const HEARTBEAT_CLOUD_INSTRUCTIONS = createHeartbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts index d6f2fcb232f12..02cd53dddbc1f 100644 --- a/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/metricbeat_instructions.ts @@ -13,268 +13,310 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createMetricbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - OSX: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'tar xzvf metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', - 'cd metricbeat-{config.kibana.version}-darwin-x86_64/', - ], - }, - DEB: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', - 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb', - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, - }), - }, - RPM: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTextPre', { - defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', - values: { - link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - }, - }), - commands: [ - 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', - 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm', - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPost', { - defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', - values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, - }), - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Metricbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Metricbeat? See the [Quick Start]({metricbeatLink}).\n\ +export const createMetricbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/metricbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + OSX: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'tar xzvf metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', + 'cd metricbeat-{config.kibana.version}-darwin-x86_64/', + ], + }, + DEB: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', + 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb', + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Quick Start]({link}).', + values: { + link: '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + }, + }), + commands: [ + 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', + 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm', + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.debTextPost', + { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Metricbeat? See the [Quick Start]({metricbeatLink}).\n\ 1. Download the Metricbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Metricbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Metricbeat as a Windows service.', - values: { - directoryName: '`metricbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - metricbeatLink: - '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Metricbeat"', '.\\install-service-metricbeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.install.windowsTextPost', - { - defaultMessage: - 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', - values: { path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`' }, - } - ), - }, - }, - START: { - OSX: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['./metricbeat setup', './metricbeat -e'], - }, - DEB: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], - }, - RPM: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], - }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Metricbeat', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\metricbeat.exe setup', 'Start-Service metricbeat'], - }, - }, - CONFIG: { - OSX: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`metricbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), - }, - DEB: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/metricbeat/metricbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + values: { + directoryName: '`metricbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + metricbeatLink: + '{config.docs.beats.metricbeat}/metricbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Metricbeat"', '.\\install-service-metricbeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`' }, + } + ), + }, }, - RPM: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTextPre', { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`/etc/metricbeat/metricbeat.yml`', - }, - }), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTextPost', { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - }), + START: { + OSX: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.osxTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['./metricbeat setup', './metricbeat -e'], + }, + DEB: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.debTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + }, + RPM: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.start.rpmTextPre', { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.start.windowsTextPre', + { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + } + ), + commands: ['.\\metricbeat.exe setup', 'Start-Service metricbeat'], + }, }, - WINDOWS: { - title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.config.windowsTextPre', - { + CONFIG: { + OSX: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.osxTextPre', { defaultMessage: 'Modify {path} to set the connection information:', values: { - path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + path: '`metricbeat.yml`', }, - } - ), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.metricbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.osxTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + DEB: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', + path: '`/etc/metricbeat/metricbeat.yml`', }, - } - ), + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.debTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + RPM: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('home.tutorials.common.metricbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.rpmTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, + WINDOWS: { + title: i18n.translate('home.tutorials.common.metricbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.metricbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createMetricbeatCloudInstructions = () => ({ CONFIG: { @@ -442,7 +484,7 @@ export function metricbeatStatusCheck(moduleName: string) { }; } -export function onPremInstructions(moduleName: string, context?: TutorialContext) { +export function onPremInstructions(moduleName: string, context: TutorialContext) { const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(context); return { @@ -498,10 +540,10 @@ export function onPremInstructions(moduleName: string, context?: TutorialContext }; } -export function onPremCloudInstructions(moduleName: string) { +export function onPremCloudInstructions(moduleName: string, context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(context); return { instructionSets: [ @@ -564,8 +606,8 @@ export function onPremCloudInstructions(moduleName: string) { }; } -export function cloudInstructions(moduleName: string) { - const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(); +export function cloudInstructions(moduleName: string, context: TutorialContext) { + const METRICBEAT_INSTRUCTIONS = createMetricbeatInstructions(context); const METRICBEAT_CLOUD_INSTRUCTIONS = createMetricbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts index 7e90795448a6c..2c33285899f65 100644 --- a/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/winlogbeat_instructions.ts @@ -13,94 +13,106 @@ import { getSpaceIdForBeatsTutorial } from './get_space_id_for_beats_tutorial'; import { TutorialContext } from '../../services/tutorials/lib/tutorials_registry_types'; import { cloudPasswordAndResetLink } from './cloud_instructions'; -export const createWinlogbeatInstructions = (context?: TutorialContext) => ({ - INSTALL: { - WINDOWS: { - title: i18n.translate('home.tutorials.common.winlogbeatInstructions.install.windowsTitle', { - defaultMessage: 'Download and install Winlogbeat', - }), - textPre: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPre', - { - defaultMessage: - 'First time using Winlogbeat? See the [Quick Start]({winlogbeatLink}).\n\ +export const createWinlogbeatInstructions = (context: TutorialContext) => { + const SSL_DOC_URL = `https://www.elastic.co/guide/en/beats/winlogbeat/${context.kibanaBranch}/configuration-ssl.html#ca-sha256`; + + return { + INSTALL: { + WINDOWS: { + title: i18n.translate('home.tutorials.common.winlogbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Winlogbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPre', + { + defaultMessage: + 'First time using Winlogbeat? See the [Quick Start]({winlogbeatLink}).\n\ 1. Download the Winlogbeat Windows zip file from the [Download]({elasticLink}) page.\n\ 2. Extract the contents of the zip file into {folderPath}.\n\ 3. Rename the {directoryName} directory to `Winlogbeat`.\n\ 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ 5. From the PowerShell prompt, run the following commands to install Winlogbeat as a Windows service.', - values: { - directoryName: '`winlogbeat-{config.kibana.version}-windows`', - folderPath: '`C:\\Program Files`', - winlogbeatLink: - '{config.docs.beats.winlogbeat}/winlogbeat-installation-configuration.html', - elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', - }, - } - ), - commands: ['cd "C:\\Program Files\\Winlogbeat"', '.\\install-service-winlogbeat.ps1'], - textPost: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPost', - { - defaultMessage: - 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', - values: { path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`' }, - } - ), + values: { + directoryName: '`winlogbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + winlogbeatLink: + '{config.docs.beats.winlogbeat}/winlogbeat-installation-configuration.html', + elasticLink: 'https://www.elastic.co/downloads/beats/winlogbeat', + }, + } + ), + commands: ['cd "C:\\Program Files\\Winlogbeat"', '.\\install-service-winlogbeat.ps1'], + textPost: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.install.windowsTextPost', + { + defaultMessage: + 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`' }, + } + ), + }, }, - }, - START: { - WINDOWS: { - title: i18n.translate('home.tutorials.common.winlogbeatInstructions.start.windowsTitle', { - defaultMessage: 'Start Winlogbeat', - }), - textPre: i18n.translate('home.tutorials.common.winlogbeatInstructions.start.windowsTextPre', { - defaultMessage: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', - }), - commands: ['.\\winlogbeat.exe setup', 'Start-Service winlogbeat'], + START: { + WINDOWS: { + title: i18n.translate('home.tutorials.common.winlogbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Winlogbeat', + }), + textPre: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.start.windowsTextPre', + { + defaultMessage: + 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + } + ), + commands: ['.\\winlogbeat.exe setup', 'Start-Service winlogbeat'], + }, }, - }, - CONFIG: { - WINDOWS: { - title: i18n.translate('home.tutorials.common.winlogbeatInstructions.config.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPre', - { - defaultMessage: 'Modify {path} to set the connection information:', - values: { - path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`', - }, - } - ), - commands: [ - 'output.elasticsearch:', - ' hosts: [""]', - ' username: "elastic"', - ' password: ""', - 'setup.kibana:', - ' host: ""', - getSpaceIdForBeatsTutorial(context), - ], - textPost: i18n.translate( - 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPost', - { - defaultMessage: - 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ -and {kibanaUrlTemplate} is the URL of Kibana.', - values: { - passwordTemplate: '``', - esUrlTemplate: '``', - kibanaUrlTemplate: '``', - }, - } - ), + CONFIG: { + WINDOWS: { + title: i18n.translate('home.tutorials.common.winlogbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPre', + { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Winlogbeat\\winlogbeat.yml`', + }, + } + ), + commands: [ + 'output.elasticsearch:', + ' hosts: [""]', + ' username: "elastic"', + ' password: ""', + " # If using Elasticsearch's default certificate", + ' ssl.ca_trusted_fingerprint: ""', + 'setup.kibana:', + ' host: ""', + getSpaceIdForBeatsTutorial(context), + ], + textPost: i18n.translate( + 'home.tutorials.common.winlogbeatInstructions.config.windowsTextPostMarkdown', + { + defaultMessage: + 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of \ + Elasticsearch, and {kibanaUrlTemplate} is the URL of Kibana. To [configure SSL]({configureSslUrl}) with the \ + default certificate generated by Elasticsearch, add its fingerprint in {esCertFingerprintTemplate}.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + configureSslUrl: SSL_DOC_URL, + esCertFingerprintTemplate: '``', + }, + } + ), + }, }, - }, -}); + }; +}; export const createWinlogbeatCloudInstructions = () => ({ CONFIG: { @@ -158,7 +170,7 @@ export function winlogbeatStatusCheck() { }; } -export function onPremInstructions(context?: TutorialContext) { +export function onPremInstructions(context: TutorialContext) { const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); return { @@ -186,10 +198,10 @@ export function onPremInstructions(context?: TutorialContext) { }; } -export function onPremCloudInstructions() { +export function onPremCloudInstructions(context: TutorialContext) { const TRYCLOUD_OPTION1 = createTrycloudOption1(); const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(); + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); return { instructionSets: [ @@ -218,8 +230,8 @@ export function onPremCloudInstructions() { }; } -export function cloudInstructions() { - const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(); +export function cloudInstructions(context: TutorialContext) { + const WINLOGBEAT_INSTRUCTIONS = createWinlogbeatInstructions(context); const WINLOGBEAT_CLOUD_INSTRUCTIONS = createWinlogbeatCloudInstructions(); return { diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index 6d298e88a2dfb..f4469de3336cc 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -60,8 +60,8 @@ export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/iptables_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/juniper_logs/index.ts b/src/plugins/home/server/tutorials/juniper_logs/index.ts index 7430e4705a5f4..a6d34d1e8447f 100644 --- a/src/plugins/home/server/tutorials/juniper_logs/index.ts +++ b/src/plugins/home/server/tutorials/juniper_logs/index.ts @@ -54,8 +54,8 @@ export function juniperLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts index 9ccc06eb222c7..6e377f3c1f295 100644 --- a/src/plugins/home/server/tutorials/kafka_logs/index.ts +++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts @@ -57,8 +57,8 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/kafka_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kafka_metrics/index.ts b/src/plugins/home/server/tutorials/kafka_metrics/index.ts index 973ec06b58fdf..5e6250989d0ab 100644 --- a/src/plugins/home/server/tutorials/kafka_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kafka_metrics/index.ts @@ -54,8 +54,8 @@ export function kafkaMetricsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kibana_logs/index.ts b/src/plugins/home/server/tutorials/kibana_logs/index.ts index 9863a53700a55..969e4972875f4 100644 --- a/src/plugins/home/server/tutorials/kibana_logs/index.ts +++ b/src/plugins/home/server/tutorials/kibana_logs/index.ts @@ -53,8 +53,8 @@ export function kibanaLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kibana_metrics/index.ts b/src/plugins/home/server/tutorials/kibana_metrics/index.ts index 3d0eb691ede51..ff8ec0eb6e43c 100644 --- a/src/plugins/home/server/tutorials/kibana_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kibana_metrics/index.ts @@ -54,8 +54,8 @@ export function kibanaMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts index 9c66125ee0cfe..acd65e0bdc69d 100644 --- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts @@ -59,8 +59,8 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/kubernetes_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['containers', 'kubernetes'], }; } diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 688ad8245b78d..5978241d7e669 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -56,8 +56,8 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['custom'], }; } diff --git a/src/plugins/home/server/tutorials/logstash_metrics/index.ts b/src/plugins/home/server/tutorials/logstash_metrics/index.ts index 9ae4bcdcecbf1..d8d7db1b464b1 100644 --- a/src/plugins/home/server/tutorials/logstash_metrics/index.ts +++ b/src/plugins/home/server/tutorials/logstash_metrics/index.ts @@ -55,8 +55,8 @@ export function logstashMetricsSpecProvider(context: TutorialContext): TutorialS }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['custom'], }; } diff --git a/src/plugins/home/server/tutorials/memcached_metrics/index.ts b/src/plugins/home/server/tutorials/memcached_metrics/index.ts index 891567f72ca7c..a48db78e89d88 100644 --- a/src/plugins/home/server/tutorials/memcached_metrics/index.ts +++ b/src/plugins/home/server/tutorials/memcached_metrics/index.ts @@ -54,8 +54,8 @@ export function memcachedMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['custom'], }; } diff --git a/src/plugins/home/server/tutorials/microsoft_logs/index.ts b/src/plugins/home/server/tutorials/microsoft_logs/index.ts index 88893e22bc9ff..39400f4661071 100644 --- a/src/plugins/home/server/tutorials/microsoft_logs/index.ts +++ b/src/plugins/home/server/tutorials/microsoft_logs/index.ts @@ -57,8 +57,8 @@ export function microsoftLogsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/microsoft_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security', 'azure'], }; } diff --git a/src/plugins/home/server/tutorials/misp_logs/index.ts b/src/plugins/home/server/tutorials/misp_logs/index.ts index ea2147a296534..4fb70aa1018f7 100644 --- a/src/plugins/home/server/tutorials/misp_logs/index.ts +++ b/src/plugins/home/server/tutorials/misp_logs/index.ts @@ -57,8 +57,8 @@ export function mispLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/misp_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security', 'azure'], }; } diff --git a/src/plugins/home/server/tutorials/mongodb_logs/index.ts b/src/plugins/home/server/tutorials/mongodb_logs/index.ts index a7f9869d440ed..28e323a2b15a9 100644 --- a/src/plugins/home/server/tutorials/mongodb_logs/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_logs/index.ts @@ -57,8 +57,8 @@ export function mongodbLogsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mongodb_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts index cc0ecc0574fa9..db843d09abfd8 100644 --- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts @@ -59,8 +59,8 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mongodb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mssql_logs/index.ts b/src/plugins/home/server/tutorials/mssql_logs/index.ts index 06cafd95283c8..5e19a2204b22c 100644 --- a/src/plugins/home/server/tutorials/mssql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mssql_logs/index.ts @@ -54,8 +54,8 @@ export function mssqlLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts index e3c9e3c338209..3e73714784f0f 100644 --- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts @@ -57,8 +57,8 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mssql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts index 12621d05d0766..963e9b63e9ba8 100644 --- a/src/plugins/home/server/tutorials/munin_metrics/index.ts +++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts @@ -54,8 +54,8 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts index b0c6f0e69dcfb..9af0a3d078cab 100644 --- a/src/plugins/home/server/tutorials/mysql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts @@ -57,8 +57,8 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mysql_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts index 09c55dc81ff84..8339561d060d6 100644 --- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts @@ -56,8 +56,8 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/mysql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts index b6ef0a192d92f..971f0c2849bda 100644 --- a/src/plugins/home/server/tutorials/nats_logs/index.ts +++ b/src/plugins/home/server/tutorials/nats_logs/index.ts @@ -58,8 +58,8 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nats_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts index 54f034ad44b19..cdd633d88140c 100644 --- a/src/plugins/home/server/tutorials/nats_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts @@ -56,8 +56,8 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nats_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/netflow_logs/index.ts b/src/plugins/home/server/tutorials/netflow_logs/index.ts index c659d9c1d31b1..7a81159503468 100644 --- a/src/plugins/home/server/tutorials/netflow_logs/index.ts +++ b/src/plugins/home/server/tutorials/netflow_logs/index.ts @@ -56,8 +56,8 @@ export function netflowLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/netscout_logs/index.ts b/src/plugins/home/server/tutorials/netscout_logs/index.ts index e6c22947f8057..2b1a469a9bbb7 100644 --- a/src/plugins/home/server/tutorials/netscout_logs/index.ts +++ b/src/plugins/home/server/tutorials/netscout_logs/index.ts @@ -54,8 +54,8 @@ export function netscoutLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts index e6f2fc4efb01c..3797f2496ee17 100644 --- a/src/plugins/home/server/tutorials/nginx_logs/index.ts +++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts @@ -57,8 +57,8 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nginx_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts index 680dd664912d3..f32e9388c1f5b 100644 --- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts @@ -61,8 +61,8 @@ which must be enabled in your Nginx installation. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/nginx_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/o365_logs/index.ts b/src/plugins/home/server/tutorials/o365_logs/index.ts index 3cd4d3a5c5e18..cbdabc7223b32 100644 --- a/src/plugins/home/server/tutorials/o365_logs/index.ts +++ b/src/plugins/home/server/tutorials/o365_logs/index.ts @@ -60,8 +60,8 @@ export function o365LogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/o365_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/okta_logs/index.ts b/src/plugins/home/server/tutorials/okta_logs/index.ts index aad18409de329..f45ffbfb800b5 100644 --- a/src/plugins/home/server/tutorials/okta_logs/index.ts +++ b/src/plugins/home/server/tutorials/okta_logs/index.ts @@ -58,8 +58,8 @@ export function oktaLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/okta_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts index 02625b341549b..d2611fb77895e 100644 --- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts +++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts @@ -48,8 +48,8 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts index 14cf5392c5231..263f2f5ab184b 100644 --- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts +++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts @@ -55,8 +55,8 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index 4f87fc4e256e1..1c77222ce43a0 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -60,8 +60,8 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/panw_logs/index.ts b/src/plugins/home/server/tutorials/panw_logs/index.ts index f5158c48f30d5..4b44038c07ade 100644 --- a/src/plugins/home/server/tutorials/panw_logs/index.ts +++ b/src/plugins/home/server/tutorials/panw_logs/index.ts @@ -60,8 +60,8 @@ export function panwLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/panw_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts index 40b35984fb17a..0a033e6378729 100644 --- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts +++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts @@ -54,8 +54,8 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts index 3a092e61b0bd9..a628f422dfb72 100644 --- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts @@ -60,8 +60,8 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/postgresql_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts index 501ea252cd16f..0ef48c33a7475 100644 --- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts @@ -56,8 +56,8 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore'], }; } diff --git a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts index 2f422e5e3be70..92a08bcce0ca4 100644 --- a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts +++ b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts @@ -55,8 +55,8 @@ export function prometheusMetricsSpecProvider(context: TutorialContext): Tutoria }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['monitoring', 'datastore'], }; } diff --git a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts index 8a1634e7da038..be6576de45a98 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts @@ -54,8 +54,8 @@ export function rabbitmqLogsSpecProvider(context: TutorialContext): TutorialSche }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts index abfc895088d91..4487a187fa373 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts @@ -60,8 +60,8 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/rabbitmq_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/radware_logs/index.ts b/src/plugins/home/server/tutorials/radware_logs/index.ts index 3e918a0a4064c..4abd897c0aff3 100644 --- a/src/plugins/home/server/tutorials/radware_logs/index.ts +++ b/src/plugins/home/server/tutorials/radware_logs/index.ts @@ -54,8 +54,8 @@ export function radwareLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts index f6aada27dec48..bb5d902d089e2 100644 --- a/src/plugins/home/server/tutorials/redis_logs/index.ts +++ b/src/plugins/home/server/tutorials/redis_logs/index.ts @@ -63,8 +63,8 @@ Note that the `slowlog` fileset is experimental. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/redis_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['datastore', 'message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts index 2bb300c48ff65..d2e8ed1efb779 100644 --- a/src/plugins/home/server/tutorials/redis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts @@ -56,8 +56,8 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/redis_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore', 'message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts index 62e1386f29dbb..85d6dce9adc52 100644 --- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts @@ -55,8 +55,8 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/redisenterprise_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore', 'message_queue'], }; } diff --git a/src/plugins/home/server/tutorials/santa_logs/index.ts b/src/plugins/home/server/tutorials/santa_logs/index.ts index da9f2e940066e..65a7bb0bd26cb 100644 --- a/src/plugins/home/server/tutorials/santa_logs/index.ts +++ b/src/plugins/home/server/tutorials/santa_logs/index.ts @@ -58,8 +58,8 @@ export function santaLogsSpecProvider(context: TutorialContext): TutorialSchema completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/santa_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security', 'os_system'], }; } diff --git a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts index 04bf7a3968320..40eb324014b15 100644 --- a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts +++ b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts @@ -54,8 +54,8 @@ export function sonicwallLogsSpecProvider(context: TutorialContext): TutorialSch }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/sophos_logs/index.ts b/src/plugins/home/server/tutorials/sophos_logs/index.ts index 4fadcecb6e1bd..c6d6f7318b6ed 100644 --- a/src/plugins/home/server/tutorials/sophos_logs/index.ts +++ b/src/plugins/home/server/tutorials/sophos_logs/index.ts @@ -54,8 +54,8 @@ export function sophosLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/squid_logs/index.ts b/src/plugins/home/server/tutorials/squid_logs/index.ts index 2d8f055d7fa6b..f325dbbd650ca 100644 --- a/src/plugins/home/server/tutorials/squid_logs/index.ts +++ b/src/plugins/home/server/tutorials/squid_logs/index.ts @@ -54,8 +54,8 @@ export function squidLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['security'], }; } diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts index 0b3c0352b663d..50f2b9dbd2e87 100644 --- a/src/plugins/home/server/tutorials/stan_metrics/index.ts +++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts @@ -56,8 +56,8 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/stan_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue', 'kubernetes'], }; } diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts index 1be010a01d5a6..c6ea0cf7ee879 100644 --- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts @@ -45,8 +45,8 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch completionTimeMinutes: 10, // previewImagePath: '', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['message_queue', 'kubernetes'], }; } diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index 373522e333379..a511be4a7a968 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -58,8 +58,8 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/suricata_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index fcc5745f48252..1de6d9df10ffb 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -56,8 +56,8 @@ export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts index 1348535d9bb72..10a6c741721b8 100644 --- a/src/plugins/home/server/tutorials/system_metrics/index.ts +++ b/src/plugins/home/server/tutorials/system_metrics/index.ts @@ -58,8 +58,8 @@ It collects system wide statistics and statistics per process and filesystem. \ completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/system_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/tomcat_logs/index.ts b/src/plugins/home/server/tutorials/tomcat_logs/index.ts index 3258d3eff5a16..2f24354742771 100644 --- a/src/plugins/home/server/tutorials/tomcat_logs/index.ts +++ b/src/plugins/home/server/tutorials/tomcat_logs/index.ts @@ -54,8 +54,8 @@ export function tomcatLogsSpecProvider(context: TutorialContext): TutorialSchema }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 30b9db4022137..7411e396a5655 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -56,8 +56,8 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts index 6f76be3056110..6e1d8d621e62e 100644 --- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts +++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts @@ -44,8 +44,8 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 118174d0e5717..9015cb4783163 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -55,8 +55,8 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/uptime_monitors/screenshot.png', onPrem: onPremInstructions([], context), - elasticCloud: cloudInstructions(), - onPremElasticCloud: onPremCloudInstructions(), + elasticCloud: cloudInstructions(context), + onPremElasticCloud: onPremCloudInstructions(context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts index b1dbeb89bdb26..bb288ba72ab02 100644 --- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts +++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts @@ -57,8 +57,8 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/uwsgi_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts index 14a574872221a..0070be6622294 100644 --- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts +++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts @@ -54,8 +54,8 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['web', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/windows_event_logs/index.ts b/src/plugins/home/server/tutorials/windows_event_logs/index.ts index 008468487ea64..baab0f4c95080 100644 --- a/src/plugins/home/server/tutorials/windows_event_logs/index.ts +++ b/src/plugins/home/server/tutorials/windows_event_logs/index.ts @@ -54,8 +54,8 @@ export function windowsEventLogsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(context), - elasticCloud: cloudInstructions(), - onPremElasticCloud: onPremCloudInstructions(), + elasticCloud: cloudInstructions(context), + onPremElasticCloud: onPremCloudInstructions(context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/windows_metrics/index.ts b/src/plugins/home/server/tutorials/windows_metrics/index.ts index 31d9b3f8962ce..ebd5e6864a229 100644 --- a/src/plugins/home/server/tutorials/windows_metrics/index.ts +++ b/src/plugins/home/server/tutorials/windows_metrics/index.ts @@ -54,8 +54,8 @@ export function windowsMetricsSpecProvider(context: TutorialContext): TutorialSc }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['os_system', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index df86518978c52..3eded8336df74 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -58,8 +58,8 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { completionTimeMinutes: 10, previewImagePath: '/plugins/home/assets/zeek_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'monitoring', 'security'], }; } diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts index 8f732969a07f3..4e4206bc1ca29 100644 --- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts +++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts @@ -55,8 +55,8 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, context), - elasticCloud: cloudInstructions(moduleName), - onPremElasticCloud: onPremCloudInstructions(moduleName), + elasticCloud: cloudInstructions(moduleName, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, context), integrationBrowserCategories: ['datastore', 'config_management'], }; } diff --git a/src/plugins/home/server/tutorials/zscaler_logs/index.ts b/src/plugins/home/server/tutorials/zscaler_logs/index.ts index 977bbb242c62a..316590c74fd76 100644 --- a/src/plugins/home/server/tutorials/zscaler_logs/index.ts +++ b/src/plugins/home/server/tutorials/zscaler_logs/index.ts @@ -54,8 +54,8 @@ export function zscalerLogsSpecProvider(context: TutorialContext): TutorialSchem }, completionTimeMinutes: 10, onPrem: onPremInstructions(moduleName, platforms, context), - elasticCloud: cloudInstructions(moduleName, platforms), - onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + elasticCloud: cloudInstructions(moduleName, platforms, context), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms, context), integrationBrowserCategories: ['network', 'security'], }; } diff --git a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx index 5d466c2f4b3c8..e20c7eb0f8c21 100644 --- a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx +++ b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx @@ -98,7 +98,14 @@ export const getHeatmapVisTypeDefinition = ({ }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -106,7 +113,14 @@ export const getHeatmapVisTypeDefinition = ({ title: i18n.translate('visTypeHeatmap.heatmap.groupTitle', { defaultMessage: 'Y-axis' }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -121,7 +135,14 @@ export const getHeatmapVisTypeDefinition = ({ }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/metric/public/metric_vis_type.ts b/src/plugins/vis_types/metric/public/metric_vis_type.ts index d4db2ac9e4671..ffb34248aeccc 100644 --- a/src/plugins/vis_types/metric/public/metric_vis_type.ts +++ b/src/plugins/vis_types/metric/public/metric_vis_type.ts @@ -86,7 +86,14 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition => }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts index 15a3675125f61..545456b6dcce0 100644 --- a/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/pie/public/sample_vis.test.mocks.ts @@ -87,7 +87,14 @@ export const samplePieVis = { title: 'Split slices', min: 0, max: null, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], editor: false, params: [], }, @@ -98,7 +105,14 @@ export const samplePieVis = { mustBeFirst: true, min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', diff --git a/src/plugins/vis_types/pie/public/vis_type/pie.ts b/src/plugins/vis_types/pie/public/vis_type/pie.ts index 0d012ed95b5d9..f10af053bd161 100644 --- a/src/plugins/vis_types/pie/public/vis_type/pie.ts +++ b/src/plugins/vis_types/pie/public/vis_type/pie.ts @@ -80,7 +80,14 @@ export const getPieVisTypeDefinition = ({ }), min: 0, max: Infinity, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -91,7 +98,14 @@ export const getPieVisTypeDefinition = ({ mustBeFirst: true, min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/table/public/table_vis_type.ts b/src/plugins/vis_types/table/public/table_vis_type.ts index a641224e23f52..2f1642e29107a 100644 --- a/src/plugins/vis_types/table/public/table_vis_type.ts +++ b/src/plugins/vis_types/table/public/table_vis_type.ts @@ -62,7 +62,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { defaultMessage: 'Split rows', }), - aggFilter: ['!filter'], + aggFilter: ['!filter', '!sampler', '!diversified_sampler', '!multi_terms'], }, { group: AggGroupNames.Buckets, @@ -72,7 +72,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!filter'], + aggFilter: ['!filter', '!sampler', '!diversified_sampler', '!multi_terms'], }, ], }, diff --git a/src/plugins/vis_types/vislib/public/gauge.ts b/src/plugins/vis_types/vislib/public/gauge.ts index 51cd7ea7622df..31a44a5d1d73f 100644 --- a/src/plugins/vis_types/vislib/public/gauge.ts +++ b/src/plugins/vis_types/vislib/public/gauge.ts @@ -132,7 +132,14 @@ export const gaugeVisTypeDefinition: VisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/vislib/public/goal.ts b/src/plugins/vis_types/vislib/public/goal.ts index 05ad1f53904d7..26bc598790839 100644 --- a/src/plugins/vis_types/vislib/public/goal.ts +++ b/src/plugins/vis_types/vislib/public/goal.ts @@ -96,7 +96,14 @@ export const goalVisTypeDefinition: VisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts index 41ab13d54f7c6..401afc5a7473a 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -625,7 +625,14 @@ export const getVis = (bucketType: string) => { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -634,7 +641,14 @@ export const getVis = (bucketType: string) => { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -643,7 +657,14 @@ export const getVis = (bucketType: string) => { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', @@ -688,7 +709,14 @@ export const getVis = (bucketType: string) => { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -697,7 +725,14 @@ export const getVis = (bucketType: string) => { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -706,7 +741,14 @@ export const getVis = (bucketType: string) => { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', @@ -722,7 +764,14 @@ export const getVis = (bucketType: string) => { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -731,7 +780,14 @@ export const getVis = (bucketType: string) => { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [], }, { @@ -740,7 +796,14 @@ export const getVis = (bucketType: string) => { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', diff --git a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts index 6077732a9cc6b..766929a2cd654 100644 --- a/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_types/xy/public/sample_vis.test.mocks.ts @@ -149,7 +149,14 @@ export const sampleAreaVis = { title: 'X-axis', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], editor: false, params: [], }, @@ -159,7 +166,14 @@ export const sampleAreaVis = { title: 'Split series', min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], editor: false, params: [], }, @@ -169,7 +183,14 @@ export const sampleAreaVis = { title: 'Split chart', min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], params: [ { name: 'row', diff --git a/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts b/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts index 67b8a1c160d40..5c22527d5b9d7 100644 --- a/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts +++ b/src/plugins/vis_types/xy/public/utils/get_series_params.test.ts @@ -45,7 +45,7 @@ describe('getSeriesParams', () => { ); expect(seriesParams).toStrictEqual([ { - circlesRadius: 3, + circlesRadius: 1, data: { id: '1', label: 'Total quantity', diff --git a/src/plugins/vis_types/xy/public/utils/get_series_params.ts b/src/plugins/vis_types/xy/public/utils/get_series_params.ts index 987c8df83b01f..0acd2a0913282 100644 --- a/src/plugins/vis_types/xy/public/utils/get_series_params.ts +++ b/src/plugins/vis_types/xy/public/utils/get_series_params.ts @@ -22,7 +22,7 @@ const makeSerie = ( type: ChartType.Line, drawLinesBetweenPoints: true, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, interpolate: InterpolationMode.Linear, lineWidth: 2, valueAxis: defaultValueAxis, diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index 3b8f78db25d36..efeb4142ff0d7 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -97,7 +97,7 @@ export const areaVisTypeDefinition = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, interpolate: InterpolationMode.Linear, valueAxis: 'ValueAxis-1', }, @@ -157,7 +157,14 @@ export const areaVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -167,7 +174,14 @@ export const areaVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -177,7 +191,14 @@ export const areaVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index 79b3fd72de452..1cd346abec6e7 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -101,7 +101,7 @@ export const histogramVisTypeDefinition = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, }, ], radiusRatio: 0, @@ -160,7 +160,14 @@ export const histogramVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -170,7 +177,14 @@ export const histogramVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -180,7 +194,14 @@ export const histogramVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index 5ac833190dd38..4e6056bbdae4f 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -102,7 +102,7 @@ export const horizontalBarVisTypeDefinition = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, }, ], addTooltip: true, @@ -159,7 +159,14 @@ export const horizontalBarVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -169,7 +176,14 @@ export const horizontalBarVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -179,7 +193,14 @@ export const horizontalBarVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index f7467ca53fa0e..affcc64320df6 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -99,7 +99,7 @@ export const lineVisTypeDefinition = { lineWidth: 2, interpolate: InterpolationMode.Linear, showCircles: true, - circlesRadius: 3, + circlesRadius: 1, }, ], addTooltip: true, @@ -151,7 +151,14 @@ export const lineVisTypeDefinition = { title: i18n.translate('visTypeXy.line.segmentTitle', { defaultMessage: 'X-axis' }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -161,7 +168,14 @@ export const lineVisTypeDefinition = { }), min: 0, max: 3, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, { group: AggGroupNames.Buckets, @@ -171,7 +185,14 @@ export const lineVisTypeDefinition = { }), min: 0, max: 1, - aggFilter: ['!geohash_grid', '!geotile_grid', '!filter', '!multi_terms'], + aggFilter: [ + '!geohash_grid', + '!geotile_grid', + '!filter', + '!sampler', + '!diversified_sampler', + '!multi_terms', + ], }, ], }, diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts new file mode 100644 index 0000000000000..c0820cce45c90 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/check_for_duplicate_title.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { OverlayStart, SavedObjectsClientContract } from 'kibana/public'; +import type { SavedObject } from '../../../../saved_objects/public'; +import { SAVE_DUPLICATE_REJECTED } from './constants'; +import { findObjectByTitle } from './find_object_by_title'; +import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; + +/** + * check for an existing SavedObject with the same title in ES + * returns Promise when it's no duplicate, or the modal displaying the warning + * that's there's a duplicate is confirmed, else it returns a rejected Promise + * @param savedObject + * @param isTitleDuplicateConfirmed + * @param onTitleDuplicate + * @param services + */ +export async function checkForDuplicateTitle( + savedObject: Pick< + SavedObject, + 'id' | 'title' | 'getDisplayName' | 'lastSavedTitle' | 'copyOnSave' | 'getEsType' + >, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: (() => void) | undefined, + services: { + savedObjectsClient: SavedObjectsClientContract; + overlays: OverlayStart; + } +): Promise { + const { savedObjectsClient, overlays } = services; + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { + return true; + } + + const duplicate = await findObjectByTitle( + savedObjectsClient, + savedObject.getEsType(), + savedObject.title + ); + + if (!duplicate || duplicate.id === savedObject.id) { + return true; + } + + if (onTitleDuplicate) { + onTitleDuplicate(); + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } + + // TODO: make onTitleDuplicate a required prop and remove UI components from this class + // Need to leave here until all users pass onTitleDuplicate. + return displayDuplicateTitleConfirmModal(savedObject, overlays); +} diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/confirm_modal_promise.tsx b/src/plugins/visualizations/public/utils/saved_objects_utils/confirm_modal_promise.tsx new file mode 100644 index 0000000000000..3c29fd958465b --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/confirm_modal_promise.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import type { OverlayStart } from 'kibana/public'; +import { EuiConfirmModal } from '@elastic/eui'; +import { toMountPoint } from '../../../../kibana_react/public'; + +export function confirmModalPromise( + message = '', + title = '', + confirmBtnText = '', + overlays: OverlayStart +): Promise { + return new Promise((resolve, reject) => { + const cancelButtonText = i18n.translate('visualizations.confirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }); + + const modal = overlays.openModal( + toMountPoint( + { + modal.close(); + reject(); + }} + onConfirm={() => { + modal.close(); + resolve(true); + }} + confirmButtonText={confirmBtnText} + cancelButtonText={cancelButtonText} + title={title} + > + {message} + + ) + ); + }); +} diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/constants.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/constants.ts new file mode 100644 index 0000000000000..fcabc0b493f68 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/constants.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +/** An error message to be used when the user rejects a confirm overwrite. */ +export const OVERWRITE_REJECTED = i18n.translate('visualizations.overwriteRejectedDescription', { + defaultMessage: 'Overwrite confirmation was rejected', +}); + +/** An error message to be used when the user rejects a confirm save with duplicate title. */ +export const SAVE_DUPLICATE_REJECTED = i18n.translate( + 'visualizations.saveDuplicateRejectedDescription', + { + defaultMessage: 'Save with duplicate title confirmation was rejected', + } +); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts new file mode 100644 index 0000000000000..48ada48511812 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/display_duplicate_title_confirm_modal.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { OverlayStart } from 'kibana/public'; +import type { SavedObject } from '../../../../saved_objects/public'; +import { SAVE_DUPLICATE_REJECTED } from './constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +export function displayDuplicateTitleConfirmModal( + savedObject: Pick, + overlays: OverlayStart +): Promise { + const confirmMessage = i18n.translate( + 'visualizations.confirmModal.saveDuplicateConfirmationMessage', + { + defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, + values: { title: savedObject.title, name: savedObject.getDisplayName() }, + } + ); + + const confirmButtonText = i18n.translate('visualizations.confirmModal.saveDuplicateButtonLabel', { + defaultMessage: 'Save {name}', + values: { name: savedObject.getDisplayName() }, + }); + try { + return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays); + } catch { + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } +} diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.test.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.test.ts new file mode 100644 index 0000000000000..d61fe1c13eee4 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { findObjectByTitle } from './find_object_by_title'; +import { + SimpleSavedObject, + SavedObjectsClientContract, + SavedObject, +} from '../../../../../core/public'; + +describe('findObjectByTitle', () => { + const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; + + beforeEach(() => { + savedObjectsClient.find = jest.fn(); + }); + + it('returns undefined if title is not provided', async () => { + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', ''); + expect(match).toBeUndefined(); + }); + + it('matches any case', async () => { + const indexPattern = new SimpleSavedObject(savedObjectsClient, { + attributes: { title: 'foo' }, + } as SavedObject); + savedObjectsClient.find = jest.fn().mockImplementation(() => + Promise.resolve({ + savedObjects: [indexPattern], + }) + ); + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', 'FOO'); + expect(match).toEqual(indexPattern); + }); +}); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts new file mode 100644 index 0000000000000..10289ac0f2f53 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/find_object_by_title.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { + SavedObjectsClientContract, + SimpleSavedObject, + SavedObjectAttributes, +} from 'kibana/public'; + +/** Returns an object matching a given title */ +export async function findObjectByTitle( + savedObjectsClient: SavedObjectsClientContract, + type: string, + title: string +): Promise | void> { + if (!title) { + return; + } + + // Elastic search will return the most relevant results first, which means exact matches should come + // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. + const response = await savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + return response.savedObjects.find( + (obj) => obj.get('title').toLowerCase() === title.toLowerCase() + ); +} diff --git a/packages/kbn-es/src/errors.js b/src/plugins/visualizations/public/utils/saved_objects_utils/index.ts similarity index 63% rename from packages/kbn-es/src/errors.js rename to src/plugins/visualizations/public/utils/saved_objects_utils/index.ts index 87490168bf5ee..e993ddd96a7d9 100644 --- a/packages/kbn-es/src/errors.js +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/index.ts @@ -6,12 +6,5 @@ * Side Public License, v 1. */ -exports.createCliError = function (message) { - const error = new Error(message); - error.isCliError = true; - return error; -}; - -exports.isCliError = function (error) { - return error && error.isCliError; -}; +export { saveWithConfirmation } from './save_with_confirmation'; +export { checkForDuplicateTitle } from './check_for_duplicate_title'; diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts new file mode 100644 index 0000000000000..6d2c8f6bbe089 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectAttributes, SavedObjectsCreateOptions, OverlayStart } from 'kibana/public'; +import type { SavedObjectsClientContract } from 'kibana/public'; +import { saveWithConfirmation } from './save_with_confirmation'; +import * as deps from './confirm_modal_promise'; +import { OVERWRITE_REJECTED } from './constants'; + +describe('saveWithConfirmation', () => { + const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; + const overlays: OverlayStart = {} as OverlayStart; + const source: SavedObjectAttributes = {} as SavedObjectAttributes; + const options: SavedObjectsCreateOptions = {} as SavedObjectsCreateOptions; + const savedObject = { + getEsType: () => 'test type', + title: 'test title', + displayName: 'test display name', + }; + + beforeEach(() => { + savedObjectsClient.create = jest.fn(); + jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.resolve({} as any)); + }); + + test('should call create of savedObjectsClient', async () => { + await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays }); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + savedObject.getEsType(), + source, + options + ); + }); + + test('should call confirmModalPromise when such record exists', async () => { + savedObjectsClient.create = jest + .fn() + .mockImplementation((type, src, opt) => + opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } }) + ); + + await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays }); + expect(deps.confirmModalPromise).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.any(String), + overlays + ); + }); + + test('should call create of savedObjectsClient when overwriting confirmed', async () => { + savedObjectsClient.create = jest + .fn() + .mockImplementation((type, src, opt) => + opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } }) + ); + + await saveWithConfirmation(source, savedObject, options, { savedObjectsClient, overlays }); + expect(savedObjectsClient.create).toHaveBeenLastCalledWith(savedObject.getEsType(), source, { + overwrite: true, + ...options, + }); + }); + + test('should reject when overwriting denied', async () => { + savedObjectsClient.create = jest.fn().mockReturnValue(Promise.reject({ res: { status: 409 } })); + jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.reject()); + + expect.assertions(1); + await expect( + saveWithConfirmation(source, savedObject, options, { + savedObjectsClient, + overlays, + }) + ).rejects.toThrow(OVERWRITE_REJECTED); + }); +}); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts new file mode 100644 index 0000000000000..de9ba38343548 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/save_with_confirmation.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import type { + SavedObjectAttributes, + SavedObjectsCreateOptions, + OverlayStart, + SavedObjectsClientContract, +} from 'kibana/public'; +import { OVERWRITE_REJECTED } from './constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +/** + * Attempts to create the current object using the serialized source. If an object already + * exists, a warning message requests an overwrite confirmation. + * @param source - serialized version of this object what will be indexed into elasticsearch. + * @param savedObject - a simple object that contains properties title and displayName, and getEsType method + * @param options - options to pass to the saved object create method + * @param services - provides Kibana services savedObjectsClient and overlays + * @returns {Promise} - A promise that is resolved with the objects id if the object is + * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with + * a confirmRejected = true parameter so that case can be handled differently than + * a create or index error. + * @resolved {SavedObject} + */ +export async function saveWithConfirmation( + source: SavedObjectAttributes, + savedObject: { + getEsType(): string; + title: string; + displayName: string; + }, + options: SavedObjectsCreateOptions, + services: { savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart } +) { + const { savedObjectsClient, overlays } = services; + try { + return await savedObjectsClient.create(savedObject.getEsType(), source, options); + } catch (err) { + // record exists, confirm overwriting + if (get(err, 'res.status') === 409) { + const confirmMessage = i18n.translate( + 'visualizations.confirmModal.overwriteConfirmationMessage', + { + defaultMessage: 'Are you sure you want to overwrite {title}?', + values: { title: savedObject.title }, + } + ); + + const title = i18n.translate('visualizations.confirmModal.overwriteTitle', { + defaultMessage: 'Overwrite {name}?', + values: { name: savedObject.displayName }, + }); + const confirmButtonText = i18n.translate('visualizations.confirmModal.overwriteButtonLabel', { + defaultMessage: 'Overwrite', + }); + + return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays) + .then(() => + savedObjectsClient.create(savedObject.getEsType(), source, { + overwrite: true, + ...options, + }) + ) + .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); + } + return await Promise.reject(err); + } +} diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts index 5c8c0594d3563..fe2453fbb78a4 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -56,10 +56,11 @@ const mockCheckForDuplicateTitle = jest.fn(() => { } }); const mockSaveWithConfirmation = jest.fn(() => ({ id: 'test-after-confirm' })); -jest.mock('../../../../plugins/saved_objects/public', () => ({ +jest.mock('./saved_objects_utils/check_for_duplicate_title', () => ({ checkForDuplicateTitle: jest.fn(() => mockCheckForDuplicateTitle()), +})); +jest.mock('./saved_objects_utils/save_with_confirmation', () => ({ saveWithConfirmation: jest.fn(() => mockSaveWithConfirmation()), - isErrorNonFatal: jest.fn(() => true), })); describe('saved_visualize_utils', () => { @@ -263,15 +264,19 @@ describe('saved_visualize_utils', () => { describe('isTitleDuplicateConfirmed', () => { it('as false we should not save vis with duplicated title', async () => { isTitleDuplicateConfirmed = false; - const savedVisId = await saveVisualization( - vis, - { isTitleDuplicateConfirmed }, - { savedObjectsClient, overlays } - ); + try { + const savedVisId = await saveVisualization( + vis, + { isTitleDuplicateConfirmed }, + { savedObjectsClient, overlays } + ); + expect(savedVisId).toBe(''); + } catch { + // ignore + } expect(savedObjectsClient.create).not.toHaveBeenCalled(); expect(mockSaveWithConfirmation).not.toHaveBeenCalled(); expect(mockCheckForDuplicateTitle).toHaveBeenCalled(); - expect(savedVisId).toBe(''); expect(vis.id).toBeUndefined(); }); diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts index a28ee9486c4d2..f221fa6a208b8 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts @@ -22,11 +22,7 @@ import { parseSearchSourceJSON, DataPublicPluginStart, } from '../../../../plugins/data/public'; -import { - checkForDuplicateTitle, - saveWithConfirmation, - isErrorNonFatal, -} from '../../../../plugins/saved_objects/public'; +import { saveWithConfirmation, checkForDuplicateTitle } from './saved_objects_utils'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry'; @@ -41,6 +37,7 @@ import type { TypesStart, BaseVisType } from '../vis_types'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { injectReferences, extractReferences } from './saved_visualization_references'; +import { OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED } from './saved_objects_utils/constants'; export const SAVED_VIS_TYPE = 'visualization'; @@ -395,7 +392,7 @@ export async function saveVisualization( return savedObject.id; } catch (err: any) { savedObject.id = originalId; - if (isErrorNonFatal(err)) { + if (err && [OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED].includes(err.message)) { return ''; } return Promise.reject(err); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 1f6fbfeb47e59..06e06a4fefa0c 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -1674,7 +1674,8 @@ describe('migration visualization', () => { type = 'area', categoryAxes?: object[], valueAxes?: object[], - hasPalette = false + hasPalette = false, + hasCirclesRadius = false ) => ({ attributes: { title: 'My Vis', @@ -1694,6 +1695,21 @@ describe('migration visualization', () => { labels: {}, }, ], + seriesParams: [ + { + show: true, + type, + mode: 'stacked', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + ...(hasCirclesRadius && { + circlesRadius: 3, + }), + }, + ], ...(hasPalette && { palette: { type: 'palette', @@ -1732,6 +1748,20 @@ describe('migration visualization', () => { expect(palette.name).toEqual('default'); }); + it("should decorate existing docs with the circlesRadius attribute if it doesn't exist", () => { + const migratedTestDoc = migrate(getTestDoc()); + const [result] = JSON.parse(migratedTestDoc.attributes.visState).params.seriesParams; + + expect(result.circlesRadius).toEqual(1); + }); + + it('should not decorate existing docs with the circlesRadius attribute if it exists', () => { + const migratedTestDoc = migrate(getTestDoc('area', undefined, undefined, true, true)); + const [result] = JSON.parse(migratedTestDoc.attributes.visState).params.seriesParams; + + expect(result.circlesRadius).toEqual(3); + }); + describe('labels.filter', () => { it('should keep existing categoryAxes labels.filter value', () => { const migratedTestDoc = migrate(getTestDoc('area', [{ labels: { filter: false } }])); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index b598d34943e6c..4c8771a2f6924 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -867,6 +867,20 @@ const decorateAxes = ( }, })); +/** + * Defaults circlesRadius to 1 if it is not configured + */ +const addCirclesRadius = (axes: T[]): T[] => + axes.map((axis) => { + const hasCircleRadiusAttribute = Number.isFinite(axis?.circlesRadius); + return { + ...axis, + ...(!hasCircleRadiusAttribute && { + circlesRadius: 1, + }), + }; + }); + // Inlined from vis_type_xy const CHART_TYPE_AREA = 'area'; const CHART_TYPE_LINE = 'line'; @@ -913,10 +927,12 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => valueAxes: visState.params.valueAxes && decorateAxes(visState.params.valueAxes, isHorizontalBar), + seriesParams: + visState.params.seriesParams && addCirclesRadius(visState.params.seriesParams), isVislibVis: true, detailedTooltip: true, ...(isLineOrArea && { - fittingFunction: 'zero', + fittingFunction: 'linear', }), }, }), diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs_sampler.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_sampler.ts new file mode 100644 index 0000000000000..d2411b2416067 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_sampler.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +export default function ({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + + describe('esaggs_sampler', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + const timeRange = { + from: '2015-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + + describe('aggSampler', () => { + it('can execute aggSampler', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggSampler id="0" enabled=true schema="bucket"} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({}); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); + const resultFromSample = result.rows[0]['col-1-1']; // check that sampler bucket doesn't produce columns + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + + it('can execute aggSampler with custom shard_size', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggSampler id="0" enabled=true schema="bucket" shard_size=20} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({ shard_size: 20 }); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); // check that sampler bucket doesn't produce columns + const resultFromSample = result.rows[0]['col-1-1']; + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + }); + + describe('aggDiversifiedSampler', () => { + it('can execute aggDiversifiedSampler', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggDiversifiedSampler id="0" enabled=true schema="bucket" field="extension.raw"} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('diversified_sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({ field: 'extension.raw' }); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); + const resultFromSample = result.rows[0]['col-1-1']; // check that sampler bucket doesn't produce columns + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + + it('can execute aggSampler with custom shard_size and max_docs_per_value', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggDiversifiedSampler id="0" enabled=true schema="bucket" field="extension.raw" shard_size=20 max_docs_per_value=3} + aggs={aggAvg id="1" enabled=true schema="metric" field="bytes"} + `; + const result = await expectExpression('sampler', expression).getResponse(); + + expect(result.columns.length).to.be(2); + const samplerColumn = result.columns[0]; + expect(samplerColumn.name).to.be('diversified_sampler'); + expect(samplerColumn.meta.sourceParams.params).to.eql({ + field: 'extension.raw', + max_docs_per_value: 3, + shard_size: 20, + }); + + expect(result.rows.length).to.be(1); + expect(Object.keys(result.rows[0]).length).to.be(1); // check that sampler bucket doesn't produce columns + const resultFromSample = result.rows[0]['col-1-1']; + expect(typeof resultFromSample).to.be('number'); + expect(resultFromSample).to.greaterThan(0); // can't check exact metric using sample + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 32f59fcf3df9c..fe2ccce23d94a 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -44,5 +44,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid loadTestFile(require.resolve('./esaggs')); loadTestFile(require.resolve('./esaggs_timeshift')); loadTestFile(require.resolve('./esaggs_multiterms')); + loadTestFile(require.resolve('./esaggs_sampler')); }); } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index d370a278e0a5c..df650b11abfc0 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -299,7 +299,6 @@ describe('Task Runner', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -450,7 +449,6 @@ describe('Task Runner', () => { const eventLogger = customTaskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -582,7 +580,6 @@ describe('Task Runner', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(5, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { alerting: { @@ -671,7 +668,6 @@ describe('Task Runner', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -767,7 +763,6 @@ describe('Task Runner', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], @@ -931,7 +926,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -1001,7 +995,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -1272,7 +1265,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -1418,7 +1410,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -1569,7 +1560,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -1755,7 +1745,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -2139,7 +2128,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2246,7 +2234,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -2481,7 +2468,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2515,7 +2501,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2590,7 +2575,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2624,7 +2608,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2708,7 +2691,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2742,7 +2724,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2826,7 +2807,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2860,7 +2840,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -2943,7 +2922,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -2977,7 +2955,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, @@ -3238,7 +3215,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -3416,7 +3392,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -3525,7 +3500,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -3631,7 +3605,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -3732,7 +3705,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -3834,7 +3806,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -3930,7 +3901,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -4036,7 +4006,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -4134,7 +4103,6 @@ describe('Task Runner', () => { Array [ Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -4234,7 +4202,6 @@ describe('Task Runner', () => { ], Array [ Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "category": Array [ @@ -4383,7 +4350,6 @@ describe('Task Runner', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { - "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute-start", "category": Array [ @@ -4463,7 +4429,6 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({ - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', kind: 'alert', @@ -4484,7 +4449,6 @@ describe('Task Runner', () => { message: 'alert execution start: "1"', }); expect(eventLogger.logEvent.mock.calls[1][0]).toStrictEqual({ - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', kind: 'alert', diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 0cf5202787392..3cd1a2d1217dc 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -602,7 +602,6 @@ export class TaskRunner< const scheduleDelay = runDate.getTime() - this.taskInstance.runAt.getTime(); const event = createAlertEventLogRecordObject({ - timestamp: runDateString, ruleId: alertId, ruleType: this.alertType as UntypedNormalizedAlertType, action: EVENT_LOG_ACTIONS.execute, @@ -747,7 +746,6 @@ export class TaskRunner< const eventLogger = this.context.eventLogger; const event: IEvent = { - '@timestamp': new Date().toISOString(), event: { action: EVENT_LOG_ACTIONS.executeTimeout, kind: 'alert', diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index c82cc0a7f21e8..eb3e22f348ed7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -196,7 +196,6 @@ describe('Task Runner Cancel', () => { expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -225,7 +224,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-timeout', category: ['alerts'], @@ -250,7 +248,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], @@ -424,7 +421,6 @@ describe('Task Runner Cancel', () => { expect(eventLogger.startTiming).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -453,7 +449,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-timeout', category: ['alerts'], @@ -479,7 +474,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], @@ -539,7 +533,6 @@ describe('Task Runner Cancel', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(6); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-start', category: ['alerts'], @@ -569,7 +562,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute-timeout', category: ['alerts'], @@ -689,7 +681,6 @@ describe('Task Runner Cancel', () => { }, }); expect(eventLogger.logEvent).toHaveBeenNthCalledWith(6, { - '@timestamp': '1970-01-01T00:00:00.000Z', event: { action: 'execute', category: ['alerts'], kind: 'alert', outcome: 'success' }, kibana: { alerting: { diff --git a/x-pack/plugins/apm/common/agent_key_types.ts b/x-pack/plugins/apm/common/agent_key_types.ts new file mode 100644 index 0000000000000..986e67d35698e --- /dev/null +++ b/x-pack/plugins/apm/common/agent_key_types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CreateApiKeyResponse { + api_key: string; + expiration?: number; + id: string; + name: string; +} diff --git a/x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts new file mode 100644 index 0000000000000..ab630decb70c8 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { DATAFEED_STATE, JOB_STATE } from '../../../ml/common'; +import { Environment } from '../environment_rt'; + +export interface ApmMlJob { + environment: Environment; + version: number; + jobId: string; + jobState?: JOB_STATE; + datafeedId?: string; + datafeedState?: DATAFEED_STATE; +} diff --git a/x-pack/plugins/apm/common/anomaly_detection/get_anomaly_detection_setup_state.ts b/x-pack/plugins/apm/common/anomaly_detection/get_anomaly_detection_setup_state.ts new file mode 100644 index 0000000000000..9ca8ddbe437fe --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/get_anomaly_detection_setup_state.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FETCH_STATUS } from '../../public/hooks/use_fetcher'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { APIReturnType } from '../../public/services/rest/createCallApmApi'; +import { ENVIRONMENT_ALL } from '../environment_filter_values'; + +export enum AnomalyDetectionSetupState { + Loading = 'pending', + Failure = 'failure', + Unknown = 'unknown', + NoJobs = 'noJobs', + NoJobsForEnvironment = 'noJobsForEnvironment', + LegacyJobs = 'legacyJobs', + UpgradeableJobs = 'upgradeableJobs', + UpToDate = 'upToDate', +} + +export function getAnomalyDetectionSetupState({ + environment, + jobs, + fetchStatus, + isAuthorized, +}: { + environment: string; + jobs: APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>['jobs']; + fetchStatus: FETCH_STATUS; + isAuthorized: boolean; +}): AnomalyDetectionSetupState { + if (!isAuthorized) { + return AnomalyDetectionSetupState.Unknown; + } + + if (fetchStatus === FETCH_STATUS.LOADING) { + return AnomalyDetectionSetupState.Loading; + } + + if (fetchStatus === FETCH_STATUS.FAILURE) { + return AnomalyDetectionSetupState.Failure; + } + + if (fetchStatus !== FETCH_STATUS.SUCCESS) { + return AnomalyDetectionSetupState.Unknown; + } + + const jobsForEnvironment = + environment === ENVIRONMENT_ALL.value + ? jobs + : jobs.filter((job) => job.environment === environment); + + const hasV1Jobs = jobs.some((job) => job.version === 1); + const hasV2Jobs = jobsForEnvironment.some((job) => job.version === 2); + const hasV3Jobs = jobsForEnvironment.some((job) => job.version === 3); + const hasAnyJobs = jobs.length > 0; + + if (hasV3Jobs) { + return AnomalyDetectionSetupState.UpToDate; + } + + if (hasV2Jobs) { + return AnomalyDetectionSetupState.UpgradeableJobs; + } + + if (hasV1Jobs) { + return AnomalyDetectionSetupState.LegacyJobs; + } + + if (hasAnyJobs) { + return AnomalyDetectionSetupState.NoJobsForEnvironment; + } + + return AnomalyDetectionSetupState.NoJobs; +} diff --git a/x-pack/plugins/apm/common/correlations/field_stats_types.ts b/x-pack/plugins/apm/common/correlations/field_stats_types.ts index 50dc7919fbd00..41f7e3c3c6649 100644 --- a/x-pack/plugins/apm/common/correlations/field_stats_types.ts +++ b/x-pack/plugins/apm/common/correlations/field_stats_types.ts @@ -8,9 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { CorrelationsParams } from './types'; -export interface FieldStatsCommonRequestParams extends CorrelationsParams { - samplerShardSize: number; -} +export type FieldStatsCommonRequestParams = CorrelationsParams; export interface Field { fieldName: string; @@ -55,3 +53,5 @@ export type FieldStats = | NumericFieldStats | KeywordFieldStats | BooleanFieldStats; + +export type FieldValueFieldStats = TopValuesStats; diff --git a/x-pack/plugins/apm/common/environment_rt.ts b/x-pack/plugins/apm/common/environment_rt.ts index 4598ffa6f6681..67d1a6ce6fa64 100644 --- a/x-pack/plugins/apm/common/environment_rt.ts +++ b/x-pack/plugins/apm/common/environment_rt.ts @@ -11,12 +11,14 @@ import { ENVIRONMENT_NOT_DEFINED, } from './environment_filter_values'; +export const environmentStringRt = t.union([ + t.literal(ENVIRONMENT_NOT_DEFINED.value), + t.literal(ENVIRONMENT_ALL.value), + nonEmptyStringRt, +]); + export const environmentRt = t.type({ - environment: t.union([ - t.literal(ENVIRONMENT_NOT_DEFINED.value), - t.literal(ENVIRONMENT_ALL.value), - nonEmptyStringRt, - ]), + environment: environmentStringRt, }); export type Environment = t.TypeOf['environment']; diff --git a/x-pack/plugins/apm/common/fleet.ts b/x-pack/plugins/apm/common/fleet.ts index 00a958952d2de..bd8c6cf2653c2 100644 --- a/x-pack/plugins/apm/common/fleet.ts +++ b/x-pack/plugins/apm/common/fleet.ts @@ -8,7 +8,7 @@ import semverParse from 'semver/functions/parse'; export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud'; -export const SUPPORTED_APM_PACKAGE_VERSION = '7.16.0'; +export const SUPPORTED_APM_PACKAGE_VERSION = '8.0.0-dev4'; // TODO update to just '8.0.0' once published export function isPrereleaseVersion(version: string) { return semverParse(version)?.prerelease?.length ?? 0 > 0; diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx index 4a05f38d8e505..f49264242e63f 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/agent_keys_table.tsx @@ -18,10 +18,10 @@ import { ConfirmDeleteModal } from './confirm_delete_modal'; interface Props { agentKeys: ApiKey[]; - refetchAgentKeys: () => void; + onKeyDelete: () => void; } -export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) { +export function AgentKeysTable({ agentKeys, onKeyDelete }: Props) { const [agentKeyToBeDeleted, setAgentKeyToBeDeleted] = useState(); const columns: Array> = [ @@ -159,7 +159,7 @@ export function AgentKeysTable({ agentKeys, refetchAgentKeys }: Props) { agentKey={agentKeyToBeDeleted} onConfirm={() => { setAgentKeyToBeDeleted(undefined); - refetchAgentKeys(); + onKeyDelete(); }} /> )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key.tsx new file mode 100644 index 0000000000000..5803e5a2a75a8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key.tsx @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiFieldText, + EuiText, + EuiFormFieldset, + EuiCheckbox, + htmlIdGenerator, +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ApmPluginStartDeps } from '../../../../plugin'; +import { CreateApiKeyResponse } from '../../../../../common/agent_key_types'; + +interface Props { + onCancel: () => void; + onSuccess: (agentKey: CreateApiKeyResponse) => void; + onError: (keyName: string) => void; +} + +export function CreateAgentKeyFlyout({ onCancel, onSuccess, onError }: Props) { + const { + services: { security }, + } = useKibana(); + + const [username, setUsername] = useState(''); + + const [formTouched, setFormTouched] = useState(false); + const [keyName, setKeyName] = useState(''); + const [agentConfigChecked, setAgentConfigChecked] = useState(true); + const [eventWriteChecked, setEventWriteChecked] = useState(true); + const [sourcemapChecked, setSourcemapChecked] = useState(true); + + const isInputInvalid = isEmpty(keyName); + const isFormInvalid = formTouched && isInputInvalid; + + const formError = i18n.translate( + 'xpack.apm.settings.agentKeys.createKeyFlyout.name.placeholder', + { defaultMessage: 'Enter a name' } + ); + + useEffect(() => { + const getCurrentUser = async () => { + try { + const authenticatedUser = await security?.authc.getCurrentUser(); + setUsername(authenticatedUser?.username || ''); + } catch { + setUsername(''); + } + }; + getCurrentUser(); + }, [security?.authc]); + + const createAgentKeyTitle = i18n.translate( + 'xpack.apm.settings.agentKeys.createKeyFlyout.createAgentKey', + { defaultMessage: 'Create agent key' } + ); + + const createAgentKey = async () => { + setFormTouched(true); + if (isInputInvalid) { + return; + } + + try { + const { agentKey } = await callApmApi({ + endpoint: 'POST /apm/agent_keys', + signal: null, + params: { + body: { + name: keyName, + sourcemap: sourcemapChecked, + event: eventWriteChecked, + agentConfig: agentConfigChecked, + }, + }, + }); + + onSuccess(agentKey); + } catch (error) { + onError(keyName); + } + }; + + return ( + + + +

{createAgentKeyTitle}

+
+
+ + + + {username && ( + + {username} + + )} + + setKeyName(e.target.value)} + isInvalid={isFormInvalid} + onBlur={() => setFormTouched(true)} + /> + + + + + setAgentConfigChecked((state) => !state)} + /> + + + + setEventWriteChecked((state) => !state)} + /> + + + + setSourcemapChecked((state) => !state)} + /> + + + + + + + + + + + {i18n.translate( + 'xpack.apm.settings.agentKeys.createKeyFlyout.cancelButton', + { + defaultMessage: 'Cancel', + } + )} + + + + + {createAgentKeyTitle} + + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key/agent_key_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key/agent_key_callout.tsx new file mode 100644 index 0000000000000..db313e35a0229 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/create_agent_key/agent_key_callout.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiSpacer, + EuiCallOut, + EuiButtonIcon, + EuiCopy, + EuiFormControlLayout, +} from '@elastic/eui'; + +interface Props { + name: string; + token: string; +} + +export function AgentKeyCallOut({ name, token }: Props) { + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.agentKeys.copyAgentKeyField.message', + { + defaultMessage: + 'Copy this key now. You will not be able to view it again.', + } + )} +

+ + {(copy) => ( + + )} + + } + > + + +
+ + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx index 23acc2e98dd73..8fb4ede96a819 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_keys/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { @@ -21,6 +21,11 @@ import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { PermissionDenied } from './prompts/permission_denied'; import { ApiKeysNotEnabled } from './prompts/api_keys_not_enabled'; import { AgentKeysTable } from './agent_keys_table'; +import { CreateAgentKeyFlyout } from './create_agent_key'; +import { AgentKeyCallOut } from './create_agent_key/agent_key_callout'; +import { CreateApiKeyResponse } from '../../../../../common/agent_key_types'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { ApiKey } from '../../../../../../security/common/model'; const INITIAL_DATA = { areApiKeysEnabled: false, @@ -28,33 +33,12 @@ const INITIAL_DATA = { }; export function AgentKeys() { - return ( - - - {i18n.translate('xpack.apm.settings.agentKeys.descriptionText', { - defaultMessage: - 'View and delete agent keys. An agent key sends requests on behalf of a user.', - })} - - - - - -

- {i18n.translate('xpack.apm.settings.agentKeys.title', { - defaultMessage: 'Agent keys', - })} -

-
-
-
- - -
- ); -} + const { toasts } = useApmPluginContext().core.notifications; + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [createdAgentKey, setCreatedAgentKey] = + useState(); -function AgentKeysContent() { const { data: { areApiKeysEnabled, canManage } = INITIAL_DATA, status: privilegesStatus, @@ -85,16 +69,112 @@ function AgentKeysContent() { ); const agentKeys = data?.agentKeys; - const isLoading = - privilegesStatus === FETCH_STATUS.LOADING || - status === FETCH_STATUS.LOADING; - const requestFailed = - privilegesStatus === FETCH_STATUS.FAILURE || - status === FETCH_STATUS.FAILURE; + return ( + + + {i18n.translate('xpack.apm.settings.agentKeys.descriptionText', { + defaultMessage: + 'View and delete agent keys. An agent key sends requests on behalf of a user.', + })} + + + + + +

+ {i18n.translate('xpack.apm.settings.agentKeys.title', { + defaultMessage: 'Agent keys', + })} +

+
+
+ {areApiKeysEnabled && canManage && !isEmpty(agentKeys) && ( + + setIsFlyoutVisible(true)} + fill={true} + iconType="plusInCircle" + > + {i18n.translate( + 'xpack.apm.settings.agentKeys.createAgentKeyButton', + { + defaultMessage: 'Create agent key', + } + )} + + + )} +
+ + {createdAgentKey && ( + + )} + {isFlyoutVisible && ( + { + setIsFlyoutVisible(false); + }} + onSuccess={(agentKey: CreateApiKeyResponse) => { + setCreatedAgentKey(agentKey); + setIsFlyoutVisible(false); + refetchAgentKeys(); + }} + onError={(keyName: string) => { + toasts.addDanger( + i18n.translate('xpack.apm.settings.agentKeys.crate.failed', { + defaultMessage: 'Error creating agent key "{keyName}"', + values: { keyName }, + }) + ); + setIsFlyoutVisible(false); + }} + /> + )} + { + setCreatedAgentKey(undefined); + refetchAgentKeys(); + }} + onCreateAgentClick={() => setIsFlyoutVisible(true)} + /> +
+ ); +} +function AgentKeysContent({ + loading, + requestFailed, + canManage, + areApiKeysEnabled, + agentKeys, + onKeyDelete, + onCreateAgentClick, +}: { + loading: boolean; + requestFailed: boolean; + canManage: boolean; + areApiKeysEnabled: boolean; + agentKeys?: ApiKey[]; + onKeyDelete: () => void; + onCreateAgentClick: () => void; +}) { if (!agentKeys) { - if (isLoading) { + if (loading) { return ( } @@ -147,7 +227,7 @@ function AgentKeysContent() { title={

{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptTitle', { - defaultMessage: 'Create your first agent key', + defaultMessage: 'Create your first key', })}

} @@ -155,12 +235,16 @@ function AgentKeysContent() {

{i18n.translate('xpack.apm.settings.agentKeys.emptyPromptBody', { defaultMessage: - 'Create agent keys to authorize requests to the APM Server.', + 'Create keys to authorize agent requests to the APM Server.', })}

} actions={ - + {i18n.translate( 'xpack.apm.settings.agentKeys.createAgentKeyButton', { @@ -175,10 +259,7 @@ function AgentKeysContent() { if (agentKeys && !isEmpty(agentKeys)) { return ( - + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 8e1064a71647f..7fd40cc4a1663 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -11,19 +11,14 @@ import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; -import { useFetcher } from '../../../../hooks/use_fetcher'; import { LicensePrompt } from '../../../shared/license_prompt'; import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { useAnomalyDetectionJobsContext } from '../../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; export type AnomalyDetectionApiResponse = APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>; -const DEFAULT_VALUE: AnomalyDetectionApiResponse = { - jobs: [], - hasLegacyJobs: false, -}; - export function AnomalyDetection() { const plugin = useApmPluginContext(); const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; @@ -33,20 +28,14 @@ export function AnomalyDetection() { const [viewAddEnvironments, setViewAddEnvironments] = useState(false); const { - refetch, - data = DEFAULT_VALUE, - status, - } = useFetcher( - (callApmApi) => { - if (canGetJobs) { - return callApmApi({ - endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`, - }); - } - }, - [canGetJobs], - { preservePreviousData: false, showToastOnError: false } - ); + anomalyDetectionJobsStatus, + anomalyDetectionJobsRefetch, + anomalyDetectionJobsData = { + jobs: [], + hasLegacyJobs: false, + } as AnomalyDetectionApiResponse, + anomalyDetectionSetupState, + } = useAnomalyDetectionJobsContext(); if (!hasValidLicense) { return ( @@ -71,9 +60,11 @@ export function AnomalyDetection() { <> {viewAddEnvironments ? ( environment)} + currentEnvironments={anomalyDetectionJobsData.jobs.map( + ({ environment }) => environment + )} onCreateJobSuccess={() => { - refetch(); + anomalyDetectionJobsRefetch(); setViewAddEnvironments(false); }} onCancel={() => { @@ -82,11 +73,15 @@ export function AnomalyDetection() { /> ) : ( { setViewAddEnvironments(true); }} + onUpdateComplete={() => { + anomalyDetectionJobsRefetch(); + }} /> )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 2e199d1d726fb..1faab4092361d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -5,27 +5,35 @@ * 2.0. */ +import { EuiSwitch } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiSpacer, EuiText, EuiTitle, + EuiToolTip, RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; +import React, { useState } from 'react'; +import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public'; +import { AnomalyDetectionSetupState } from '../../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useMlManageJobsHref } from '../../../../hooks/use_ml_manage_jobs_href'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { MLExplorerLink } from '../../../shared/Links/MachineLearningLinks/MLExplorerLink'; import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; +import { MLCallout, shouldDisplayMlCallout } from '../../../shared/ml_callout'; import { AnomalyDetectionApiResponse } from './index'; -import { LegacyJobsCallout } from './legacy_jobs_callout'; -import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public'; +import { JobsListStatus } from './jobs_list_status'; type Jobs = AnomalyDetectionApiResponse['jobs']; @@ -36,7 +44,24 @@ const columns: Array> = [ 'xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel', { defaultMessage: 'Environment' } ), - render: getEnvironmentLabel, + width: '100%', + render: (_, { environment, jobId, jobState, datafeedState, version }) => { + return ( + + + {getEnvironmentLabel(environment)} + + + + + + ); + }, }, { field: 'job_id', @@ -45,30 +70,79 @@ const columns: Array> = [ 'xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel', { defaultMessage: 'Action' } ), - render: (_, { job_id: jobId }) => ( - - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText', - { - defaultMessage: 'View job in ML', - } - )} - - ), + render: (_, { jobId }) => { + return ( + + + + {/* setting the key to remount the element as a workaround for https://github.com/elastic/kibana/issues/119951*/} + + + + + + + + + + + + + + ); + }, }, ]; interface Props { data: AnomalyDetectionApiResponse; + setupState: AnomalyDetectionSetupState; status: FETCH_STATUS; onAddEnvironments: () => void; + onUpdateComplete: () => void; } -export function JobsList({ data, status, onAddEnvironments }: Props) { - const { jobs, hasLegacyJobs } = data; + +export function JobsList({ + data, + status, + onAddEnvironments, + setupState, + onUpdateComplete, +}: Props) { + const { core } = useApmPluginContext(); + + const { jobs } = data; + + // default to showing legacy jobs if not up to date + const [showLegacyJobs, setShowLegacyJobs] = useState( + setupState !== AnomalyDetectionSetupState.UpToDate + ); + + const mlManageJobsHref = useMlManageJobsHref(); + + const displayMlCallout = shouldDisplayMlCallout(setupState); + + const filteredJobs = showLegacyJobs + ? jobs + : jobs.filter((job) => job.version >= 3); return ( <> - j.job_id)} /> + j.jobId)} /> + {displayMlCallout && ( + <> + { + onAddEnvironments(); + }} + onUpgradeClick={() => { + if (setupState === AnomalyDetectionSetupState.UpgradeableJobs) { + return callApmApi({ + endpoint: + 'POST /internal/apm/settings/anomaly-detection/update_to_v3', + signal: null, + }).then(() => { + core.notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.jobsList.updateCompletedToastTitle', + { + defaultMessage: 'Anomaly detection jobs created!', + } + ), + text: i18n.translate( + 'xpack.apm.jobsList.updateCompletedToastText', + { + defaultMessage: + 'Your new anomaly detection jobs have been created successfully. You will start to see anomaly detection results in the app within minutes. The old jobs have been closed but the results are still available within Machine Learning.', + } + ), + }); + onUpdateComplete(); + }); + } + }} + anomalyDetectionSetupState={setupState} + /> + + + )} - +

@@ -103,12 +215,36 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {

+ + { + setShowLegacyJobs(e.target.checked); + }} + label={i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.showLegacyJobsCheckboxText', + { + defaultMessage: 'Show legacy jobs', + } + )} + /> + + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.manageMlJobsButtonText', + { + defaultMessage: 'Manage jobs', + } + )} + + {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.addEnvironments', { - defaultMessage: 'Create ML Job', + defaultMessage: 'Create job', } )} @@ -120,11 +256,10 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - - {hasLegacyJobs && } ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx new file mode 100644 index 0000000000000..6145e9f9ca7da --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DATAFEED_STATE, JOB_STATE } from '../../../../../../ml/common'; +import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; + +export function JobsListStatus({ + jobId, + jobState, + datafeedState, + version, +}: { + jobId: string; + jobState?: JOB_STATE; + datafeedState?: DATAFEED_STATE; + version: number; +}) { + const jobIsOk = + jobState === JOB_STATE.OPENED || jobState === JOB_STATE.OPENING; + + const datafeedIsOk = + datafeedState === DATAFEED_STATE.STARTED || + datafeedState === DATAFEED_STATE.STARTING; + + const isClosed = + jobState === JOB_STATE.CLOSED || jobState === JOB_STATE.CLOSING; + + const isLegacy = version < 3; + + const statuses: React.ReactElement[] = []; + + if (jobIsOk && datafeedIsOk) { + statuses.push( + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.okStatusLabel', + { defaultMessage: 'OK' } + )} + + ); + } else if (!isClosed) { + statuses.push( + + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.warningStatusBadgeLabel', + { defaultMessage: 'Warning' } + )} + + + + ); + } + + if (isClosed) { + statuses.push( + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.closedStatusLabel', + { defaultMessage: 'Closed' } + )} + + ); + } + + if (isLegacy) { + statuses.push( + + {' '} + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.legacyStatusLabel', + { defaultMessage: 'Legacy' } + )} + + ); + } + + return ( + + {statuses.map((status, idx) => ( + + {status} + + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx deleted file mode 100644 index 0d3da5c9f97ad..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiButton } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useMlHref } from '../../../../../../ml/public'; - -export function LegacyJobsCallout() { - const { - core, - plugins: { ml }, - } = useApmPluginContext(); - const mlADLink = useMlHref(ml, core.http.basePath.get(), { - page: 'jobs', - pageState: { - jobId: 'high_mean_response_time', - }, - }); - - return ( - -

- {i18n.translate( - 'xpack.apm.settings.anomaly_detection.legacy_jobs.body', - { - defaultMessage: - 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app', - } - )} -

- - {i18n.translate( - 'xpack.apm.settings.anomaly_detection.legacy_jobs.button', - { defaultMessage: 'Review jobs' } - )} - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx index f1d0d194749c5..d7043ea669a03 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/context_popover.tsx @@ -11,14 +11,11 @@ import { EuiFlexItem, EuiPopover, EuiPopoverTitle, - EuiSpacer, - EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui'; -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { FieldStats } from '../../../../../common/correlations/field_stats_types'; import { OnAddFilter, TopValues } from './top_values'; import { useTheme } from '../../../../hooks/use_theme'; @@ -97,27 +94,11 @@ export function CorrelationsContextPopover({ {infoIsOpen ? ( - <> - - {topValueStats.topValuesSampleSize !== undefined && ( - - - - - - - )} - + ) : null} ); diff --git a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx index 05b4f6d56fa45..fbf33899a2de2 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/context_popover/top_values.tsx @@ -12,11 +12,21 @@ import { EuiProgress, EuiSpacer, EuiToolTip, + EuiText, + EuiHorizontalRule, + EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldStats } from '../../../../../common/correlations/field_stats_types'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + FieldStats, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; import { asPercent } from '../../../../../common/utils/formatters'; import { useTheme } from '../../../../hooks/use_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { useFetchParams } from '../use_fetch_params'; export type OnAddFilter = ({ fieldName, @@ -28,23 +38,179 @@ export type OnAddFilter = ({ include: boolean; }) => void; -interface Props { +interface TopValueProps { + progressBarMax: number; + barColor: string; + value: TopValueBucket; + isHighlighted: boolean; + fieldName: string; + onAddFilter?: OnAddFilter; + valueText?: string; + reverseLabel?: boolean; +} +export function TopValue({ + progressBarMax, + barColor, + value, + isHighlighted, + fieldName, + onAddFilter, + valueText, + reverseLabel = false, +}: TopValueProps) { + const theme = useTheme(); + return ( + + + + {value.key} + + } + className="eui-textTruncate" + aria-label={value.key.toString()} + valueText={valueText} + labelProps={ + isHighlighted + ? { + style: { fontWeight: 'bold' }, + } + : undefined + } + /> + + {fieldName !== undefined && + value.key !== undefined && + onAddFilter !== undefined ? ( + <> + { + onAddFilter({ + fieldName, + fieldValue: + typeof value.key === 'number' + ? value.key.toString() + : value.key, + include: true, + }); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel', + { + defaultMessage: 'Filter for {fieldName}: "{value}"', + values: { fieldName, value: value.key }, + } + )} + data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`} + style={{ + minHeight: 'auto', + width: theme.eui.euiSizeL, + paddingRight: 2, + paddingLeft: 2, + paddingTop: 0, + paddingBottom: 0, + }} + /> + { + onAddFilter({ + fieldName, + fieldValue: + typeof value.key === 'number' + ? value.key.toString() + : value.key, + include: false, + }); + }} + aria-label={i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel', + { + defaultMessage: 'Filter out {fieldName}: "{value}"', + values: { fieldName, value: value.key }, + } + )} + data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`} + style={{ + minHeight: 'auto', + width: theme.eui.euiSizeL, + paddingTop: 0, + paddingBottom: 0, + paddingRight: 2, + paddingLeft: 2, + }} + /> + + ) : null} + + ); +} + +interface TopValuesProps { topValueStats: FieldStats; compressed?: boolean; onAddFilter?: OnAddFilter; fieldValue?: string | number; } -export function TopValues({ topValueStats, onAddFilter, fieldValue }: Props) { +export function TopValues({ + topValueStats, + onAddFilter, + fieldValue, +}: TopValuesProps) { const { topValues, topValuesSampleSize, count, fieldName } = topValueStats; const theme = useTheme(); - if (!Array.isArray(topValues) || topValues.length === 0) return null; + const idxToHighlight = Array.isArray(topValues) + ? topValues.findIndex((value) => value.key === fieldValue) + : null; + + const params = useFetchParams(); + const { data: fieldValueStats, status } = useFetcher( + (callApmApi) => { + if ( + idxToHighlight === -1 && + fieldName !== undefined && + fieldValue !== undefined + ) { + return callApmApi({ + endpoint: 'GET /internal/apm/correlations/field_value_stats', + params: { + query: { + ...params, + fieldName, + fieldValue, + }, + }, + }); + } + }, + [params, fieldName, fieldValue, idxToHighlight] + ); + if ( + !Array.isArray(topValues) || + topValues?.length === 0 || + fieldValue === undefined + ) + return null; const sampledSize = typeof topValuesSampleSize === 'string' ? parseInt(topValuesSampleSize, 10) : topValuesSampleSize; + const progressBarMax = sampledSize ?? count; return (
- - - - {value.key} - - } - className="eui-textTruncate" - aria-label={value.key.toString()} - valueText={valueText} - labelProps={ - isHighlighted - ? { - style: { fontWeight: 'bold' }, - } - : undefined - } - /> - - {fieldName !== undefined && - value.key !== undefined && - onAddFilter !== undefined ? ( - <> - { - onAddFilter({ - fieldName, - fieldValue: - typeof value.key === 'number' - ? value.key.toString() - : value.key, - include: true, - }); - }} - aria-label={i18n.translate( - 'xpack.apm.correlations.fieldContextPopover.addFilterAriaLabel', - { - defaultMessage: 'Filter for {fieldName}: "{value}"', - values: { fieldName, value: value.key }, - } - )} - data-test-subj={`apmFieldContextTopValuesAddFilterButton-${value.key}-${value.key}`} - style={{ - minHeight: 'auto', - width: theme.eui.euiSizeL, - paddingRight: 2, - paddingLeft: 2, - paddingTop: 0, - paddingBottom: 0, - }} - /> - { - onAddFilter({ - fieldName, - fieldValue: - typeof value.key === 'number' - ? value.key.toString() - : value.key, - include: false, - }); - }} - aria-label={i18n.translate( - 'xpack.apm.correlations.fieldContextPopover.removeFilterAriaLabel', - { - defaultMessage: 'Filter out {fieldName}: "{value}"', - values: { fieldName, value: value.key }, - } - )} - data-test-subj={`apmFieldContextTopValuesExcludeFilterButton-${value.key}-${value.key}`} - style={{ - minHeight: 'auto', - width: theme.eui.euiSizeL, - paddingTop: 0, - paddingBottom: 0, - paddingRight: 2, - paddingLeft: 2, - }} - /> - - ) : null} - + ); })} + + {idxToHighlight === -1 && ( + <> + + + + + + {status === FETCH_STATUS.SUCCESS && + Array.isArray(fieldValueStats?.topValues) ? ( + fieldValueStats?.topValues.map((value) => { + const valueText = + progressBarMax !== undefined + ? asPercent(value.doc_count, progressBarMax) + : undefined; + + return ( + + ); + }) + ) : ( + + + + )} + + )} + + {topValueStats.topValuesSampleSize !== undefined && ( + <> + + + {i18n.translate( + 'xpack.apm.correlations.fieldContextPopover.calculatedFromSampleDescription', + { + defaultMessage: + 'Calculated from sample of {sampleSize} documents', + values: { sampleSize: topValueStats.topValuesSampleSize }, + } + )} + + + )}
); } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 6ca632eac4f2e..1994d3641ee53 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -26,7 +26,7 @@ import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; import { ServiceList } from './service_list'; -import { MLCallout } from './service_list/MLCallout'; +import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout'; const initialData = { requestId: '', @@ -159,26 +159,19 @@ function useServicesFetcher() { } export function ServiceInventory() { - const { core } = useApmPluginContext(); - const { mainStatisticsData, mainStatisticsStatus, comparisonData } = useServicesFetcher(); - const { anomalyDetectionJobsData, anomalyDetectionJobsStatus } = - useAnomalyDetectionJobsContext(); + const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext(); const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( - 'apm.userHasDismissedServiceInventoryMlCallout', + `apm.userHasDismissedServiceInventoryMlCallout.${anomalyDetectionSetupState}`, false ); - const canCreateJob = !!core.application.capabilities.ml?.canCreateJob; - const displayMlCallout = - anomalyDetectionJobsStatus === FETCH_STATUS.SUCCESS && - !anomalyDetectionJobsData?.jobs.length && - canCreateJob && - !userHasDismissedCallout; + !userHasDismissedCallout && + shouldDisplayMlCallout(anomalyDetectionSetupState); const isLoading = mainStatisticsStatus === FETCH_STATUS.LOADING; const isFailure = mainStatisticsStatus === FETCH_STATUS.FAILURE; @@ -198,10 +191,14 @@ export function ServiceInventory() { return ( <> - + {displayMlCallout && ( - setUserHasDismissedCallout(true)} /> + setUserHasDismissedCallout(true)} + /> )} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx index 0a4adc07e1a98..bececfb545ba9 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from '../../../../../../../src/core/public'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { AnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; @@ -45,6 +46,7 @@ const stories: Meta<{}> = { anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, anomalyDetectionJobsRefetch: () => {}, + anomalyDetectionSetupState: AnomalyDetectionSetupState.NoJobs, }; return ( diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx deleted file mode 100644 index 91625af7062cc..0000000000000 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { EuiButton } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGrid } from '@elastic/eui'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { APMLink } from '../../../shared/Links/apm/APMLink'; - -export function MLCallout({ onDismiss }: { onDismiss: () => void }) { - return ( - -

- {i18n.translate('xpack.apm.serviceOverview.mlNudgeMessage.content', { - defaultMessage: `Pinpoint anomalous transactions and see the health of upstream and downstream services with APM's anomaly detection integration. Get started in just a few minutes.`, - })} -

- - - - - {i18n.translate( - 'xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton', - { - defaultMessage: `Get started`, - } - )} - - - - - onDismiss()}> - {i18n.translate( - 'xpack.apm.serviceOverview.mlNudgeMessage.dismissButton', - { - defaultMessage: `Dismiss`, - } - )} - - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index ea65c837a4177..fe91b14e64e8a 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -66,9 +66,11 @@ export function getServiceColumns({ showTransactionTypeColumn, comparisonData, breakpoints, + showHealthStatusColumn, }: { query: TypeOf['query']; showTransactionTypeColumn: boolean; + showHealthStatusColumn: boolean; breakpoints: Breakpoints; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { @@ -76,21 +78,25 @@ export function getServiceColumns({ const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge; const showWhenSmallOrGreaterThanXL = isSmall || !isXl; return [ - { - field: 'healthStatus', - name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { - defaultMessage: 'Health', - }), - width: `${unit * 6}px`, - sortable: true, - render: (_, { healthStatus }) => { - return ( - - ); - }, - }, + ...(showHealthStatusColumn + ? [ + { + field: 'healthStatus', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: `${unit * 6}px`, + sortable: true, + render: (_, { healthStatus }) => { + return ( + + ); + }, + } as ITableColumn, + ] + : []), { field: 'serviceName', name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { @@ -248,13 +254,17 @@ export function ServiceList({ showTransactionTypeColumn, comparisonData, breakpoints, + showHealthStatusColumn: displayHealthStatus, }), - [query, showTransactionTypeColumn, comparisonData, breakpoints] + [ + query, + showTransactionTypeColumn, + comparisonData, + breakpoints, + displayHealthStatus, + ] ); - const columns = displayHealthStatus - ? serviceColumns - : serviceColumns.filter((column) => column.field !== 'healthStatus'); const initialSortField = displayHealthStatus ? 'healthStatus' : 'transactionsPerMinute'; @@ -300,7 +310,7 @@ export function ServiceList({ { it('renders empty state', async () => { @@ -29,34 +55,10 @@ describe('ServiceList', () => { }); describe('responsive columns', () => { - const query = { - rangeFrom: 'now-15m', - rangeTo: 'now', - environment: ENVIRONMENT_ALL.value, - kuery: '', - }; - - const service: any = { - serviceName: 'opbeans-python', - agentName: 'python', - transactionsPerMinute: { - value: 86.93333333333334, - timeseries: [], - }, - errorsPerMinute: { - value: 12.6, - timeseries: [], - }, - avgResponseTime: { - value: 91535.42944785276, - timeseries: [], - }, - environments: ['test'], - transactionType: 'request', - }; describe('when small', () => { it('shows environment, transaction type and sparklines', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -91,6 +93,7 @@ describe('ServiceList', () => { describe('when Large', () => { it('hides environment, transaction type and sparklines', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -114,6 +117,7 @@ describe('ServiceList', () => { describe('when XL', () => { it('hides transaction type', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -147,6 +151,7 @@ describe('ServiceList', () => { describe('when XXL', () => { it('hides transaction type', () => { const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, query, showTransactionTypeColumn: true, breakpoints: { @@ -181,20 +186,34 @@ describe('ServiceList', () => { }); describe('without ML data', () => { - it('sorts by throughput', async () => { - render(); - - expect(await screen.findByTitle('Throughput')).toBeInTheDocument(); + it('hides healthStatus column', () => { + const renderedColumns = getServiceColumns({ + showHealthStatusColumn: false, + query, + showTransactionTypeColumn: true, + breakpoints: { + isSmall: false, + isLarge: false, + isXl: false, + } as Breakpoints, + }).map((c) => c.field); + expect(renderedColumns.includes('healthStatus')).toBeFalsy(); }); }); describe('with ML data', () => { - it('renders the health column', async () => { - render(); - - expect( - await screen.findByRole('button', { name: /Health/ }) - ).toBeInTheDocument(); + it('shows healthStatus column', () => { + const renderedColumns = getServiceColumns({ + showHealthStatusColumn: true, + query, + showTransactionTypeColumn: true, + breakpoints: { + isSmall: false, + isLarge: false, + isXl: false, + } as Breakpoints, + }).map((c) => c.field); + expect(renderedColumns.includes('healthStatus')).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx index 8f66658785b97..a82fa3121bb3b 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx @@ -12,19 +12,29 @@ import { EuiSpacer, EuiText, EuiCodeBlock, + EuiTabbedContent, + EuiBetaBadge, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { CreateAgentInstructions } from './agent_instructions_mappings'; +import React, { ComponentType } from 'react'; +import styled from 'styled-components'; +import { + AgentRuntimeAttachmentProps, + CreateAgentInstructions, +} from './agent_instructions_mappings'; import { Markdown, useKibana, } from '../../../../../../../src/plugins/kibana_react/public'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { AgentIcon } from '../../shared/agent_icon'; -import { NewPackagePolicy } from '../apm_policy_form/typings'; +import type { + NewPackagePolicy, + PackagePolicy, + PackagePolicyEditExtensionComponentProps, +} from '../apm_policy_form/typings'; import { getCommands } from '../../../tutorial/config_agent/commands/get_commands'; -import { replaceTemplateStrings } from './replace_template_strings'; +import { renderMustache } from './render_mustache'; function AccordionButtonContent({ agentName, @@ -97,96 +107,175 @@ function TutorialConfigAgent({ } interface Props { + policy: PackagePolicy; newPolicy: NewPackagePolicy; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; agentName: AgentName; title: string; variantId: string; createAgentInstructions: CreateAgentInstructions; + AgentRuntimeAttachment?: ComponentType; } +const StyledEuiAccordion = styled(EuiAccordion)` + // This is an alternative fix suggested by the EUI team to fix drag elements inside EuiAccordion + // This Issue tracks the fix on the Eui side https://github.com/elastic/eui/issues/3548#issuecomment-639041283 + .euiAccordion__childWrapper { + transform: none; + } +`; + export function AgentInstructionsAccordion({ + policy, newPolicy, + onChange, agentName, title, createAgentInstructions, variantId, + AgentRuntimeAttachment, }: Props) { const docLinks = useKibana().services.docLinks; const vars = newPolicy?.inputs?.[0]?.vars; const apmServerUrl = vars?.url.value; const secretToken = vars?.secret_token.value; const steps = createAgentInstructions(apmServerUrl, secretToken); + const stepsElements = steps.map( + ( + { title: stepTitle, textPre, textPost, customComponentName, commands }, + index + ) => { + const commandBlock = commands + ? renderMustache({ + text: commands, + docLinks, + }) + : ''; + + return ( +
+ +

{stepTitle}

+
+ + + {textPre && ( + + )} + {commandBlock && ( + <> + + + {commandBlock} + + + )} + {customComponentName === 'TutorialConfigAgent' && ( + + )} + {customComponentName === 'TutorialConfigAgentRumScript' && ( + + )} + {textPost && ( + <> + + + + )} + + +
+ ); + } + ); + + const manualInstrumentationContent = ( + <> + + {stepsElements} + + ); + return ( - } > - - {steps.map( - ( - { - title: stepTitle, - textPre, - textPost, - customComponentName, - commands, - }, - index - ) => { - const commandBlock = replaceTemplateStrings( - Array.isArray(commands) ? commands.join('\n') : commands || '', - docLinks - ); - return ( -
- -

{stepTitle}

-
- - - {textPre && ( - - )} - {commandBlock && ( - <> - - - {commandBlock} - - - )} - {customComponentName === 'TutorialConfigAgent' && ( - - )} - {customComponentName === 'TutorialConfigAgentRumScript' && ( - - )} - {textPost && ( + {AgentRuntimeAttachment ? ( + <> + + + + {i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.autoAttachment', + { defaultMessage: 'Auto-Attachment' } + )} + + + + + + ), + content: ( <> - - )} - - -
- ); - } + ), + }, + ]} + /> + + ) : ( + manualInstrumentationContent )} -
+ ); } diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts index 8bfdafe61d44e..5e992094ac64c 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_mappings.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ComponentType } from 'react'; import { createDotNetAgentInstructions, createDjangoAgentInstructions, @@ -18,6 +19,18 @@ import { createRackAgentInstructions, } from '../../../../common/tutorial/instructions/apm_agent_instructions'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { JavaRuntimeAttachment } from './runtime_attachment/supported_agents/java_runtime_attachment'; +import { + NewPackagePolicy, + PackagePolicy, + PackagePolicyEditExtensionComponentProps, +} from '../apm_policy_form/typings'; + +export interface AgentRuntimeAttachmentProps { + policy: PackagePolicy; + newPolicy: NewPackagePolicy; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; +} export type CreateAgentInstructions = ( apmServerUrl?: string, @@ -35,12 +48,14 @@ export const ApmAgentInstructionsMappings: Array<{ title: string; variantId: string; createAgentInstructions: CreateAgentInstructions; + AgentRuntimeAttachment?: ComponentType; }> = [ { agentName: 'java', title: 'Java', variantId: 'java', createAgentInstructions: createJavaAgentInstructions, + AgentRuntimeAttachment: JavaRuntimeAttachment, }, { agentName: 'rum-js', diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx index d6a43a1e1268a..09b638fb184df 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/index.tsx @@ -21,19 +21,28 @@ interface Props { onChange: PackagePolicyEditExtensionComponentProps['onChange']; } -export function ApmAgents({ newPolicy }: Props) { +export function ApmAgents({ policy, newPolicy, onChange }: Props) { return (
{ApmAgentInstructionsMappings.map( - ({ agentName, title, createAgentInstructions, variantId }) => ( + ({ + agentName, + title, + createAgentInstructions, + variantId, + AgentRuntimeAttachment, + }) => ( diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/render_mustache.ts similarity index 65% rename from x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts rename to x-pack/plugins/apm/public/components/fleet_integration/apm_agents/render_mustache.ts index d36d76d466308..ebf5fea7f2b85 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/replace_template_strings.ts +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/render_mustache.ts @@ -10,12 +10,17 @@ import Mustache from 'mustache'; const TEMPLATE_TAGS = ['{', '}']; -export function replaceTemplateStrings( - text: string, - docLinks?: CoreStart['docLinks'] -) { - Mustache.parse(text, TEMPLATE_TAGS); - return Mustache.render(text, { +export function renderMustache({ + text, + docLinks, +}: { + text: string | string[]; + docLinks?: CoreStart['docLinks']; +}) { + const template = Array.isArray(text) ? text.join('\n') : text; + + Mustache.parse(template, TEMPLATE_TAGS); + return Mustache.render(template, { config: { docs: { base_url: docLinks?.ELASTIC_WEBSITE_URL, diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/default_discovery_rule.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/default_discovery_rule.tsx new file mode 100644 index 0000000000000..848582bb3feb6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/default_discovery_rule.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiBadge, +} from '@elastic/eui'; +import React from 'react'; + +export function DefaultDiscoveryRule() { + return ( + + + + Exclude + + + Everything else + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/discovery_rule.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/discovery_rule.tsx new file mode 100644 index 0000000000000..f7b1b3db3a4c4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/discovery_rule.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiBadge, + EuiPanel, + DraggableProvidedDragHandleProps, + EuiButtonIcon, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { Operation } from '.'; + +interface Props { + id: string; + order: number; + operation: string; + type: string; + probe: string; + providedDragHandleProps?: DraggableProvidedDragHandleProps; + onDelete: (discoveryItemId: string) => void; + onEdit: (discoveryItemId: string) => void; + operationTypes: Operation[]; +} + +export function DiscoveryRule({ + id, + order, + operation, + type, + probe, + providedDragHandleProps, + onDelete, + onEdit, + operationTypes, +}: Props) { + const operationTypesLabels = useMemo(() => { + return operationTypes.reduce<{ + [operationValue: string]: { + label: string; + types: { [typeValue: string]: string }; + }; + }>((acc, current) => { + return { + ...acc, + [current.operation.value]: { + label: current.operation.label, + types: current.types.reduce((memo, { value, label }) => { + return { ...memo, [value]: label }; + }, {}), + }, + }; + }, {}); + }, [operationTypes]); + return ( + + + +
+ +
+
+ + + + {order} + + + + {operationTypesLabels[operation].label} + + + + + + +

{operationTypesLabels[operation].types[type]}

+
+
+ + {probe} + +
+
+ + + + { + onEdit(id); + }} + /> + + + { + onDelete(id); + }} + /> + + + +
+
+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/edit_discovery_rule.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/edit_discovery_rule.tsx new file mode 100644 index 0000000000000..5059bbabfce91 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/edit_discovery_rule.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiButton, + EuiButtonEmpty, + EuiFormFieldset, + EuiSelect, + EuiFieldText, + EuiFormRow, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + Operation, + DISCOVERY_RULE_TYPE_ALL, + STAGED_DISCOVERY_RULE_ID, +} from '.'; + +interface Props { + id: string; + onChangeOperation: (discoveryItemId: string) => void; + operation: string; + onChangeType: (discoveryItemId: string) => void; + type: string; + onChangeProbe: (discoveryItemId: string) => void; + probe: string; + onCancel: () => void; + onSubmit: () => void; + operationTypes: Operation[]; +} + +export function EditDiscoveryRule({ + id, + onChangeOperation, + operation, + onChangeType, + type, + onChangeProbe, + probe, + onCancel, + onSubmit, + operationTypes, +}: Props) { + return ( + + + + + ({ + text: item.operation.label, + value: item.operation.value, + }))} + value={operation} + onChange={(e) => { + onChangeOperation(e.target.value); + }} + /> + + + + + + + + + definedOperation.value === operation + ) + ?.types.map((item) => ({ + inputDisplay: item.label, + value: item.value, + dropdownDisplay: ( + <> + {item.label} + +

{item.description}

+
+ + ), + })) ?? [] + } + valueOfSelected={type} + onChange={onChangeType} + /> +
+
+
+
+ {type !== DISCOVERY_RULE_TYPE_ALL && ( + + + + + onChangeProbe(e.target.value)} + /> + + + + + )} + + + Cancel + + + + {id === STAGED_DISCOVERY_RULE_ID + ? i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.add', + { defaultMessage: 'Add' } + ) + : i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.save', + { defaultMessage: 'Save' } + )} + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/index.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/index.tsx new file mode 100644 index 0000000000000..8f2a1d3d1dea1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/index.tsx @@ -0,0 +1,327 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + htmlIdGenerator, + euiDragDropReorder, + DropResult, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import React, { useState, useCallback, ReactNode } from 'react'; +import { RuntimeAttachment as RuntimeAttachmentStateless } from './runtime_attachment'; + +export const STAGED_DISCOVERY_RULE_ID = 'STAGED_DISCOVERY_RULE_ID'; +export const DISCOVERY_RULE_TYPE_ALL = 'all'; + +export interface IDiscoveryRule { + operation: string; + type: string; + probe: string; +} + +export type IDiscoveryRuleList = Array<{ + id: string; + discoveryRule: IDiscoveryRule; +}>; + +export interface RuntimeAttachmentSettings { + enabled: boolean; + discoveryRules: IDiscoveryRule[]; + version: string | null; +} + +interface Props { + onChange?: (runtimeAttachmentSettings: RuntimeAttachmentSettings) => void; + toggleDescription: ReactNode; + discoveryRulesDescription: ReactNode; + showUnsavedWarning?: boolean; + initialIsEnabled?: boolean; + initialDiscoveryRules?: IDiscoveryRule[]; + operationTypes: Operation[]; + selectedVersion: string; + versions: string[]; +} + +interface Option { + value: string; + label: string; + description?: string; +} + +export interface Operation { + operation: Option; + types: Option[]; +} + +const versionRegex = new RegExp(/^\d+\.\d+\.\d+$/); +function validateVersion(version: string) { + return versionRegex.test(version); +} + +export function RuntimeAttachment(props: Props) { + const { initialDiscoveryRules = [], onChange = () => {} } = props; + const [isEnabled, setIsEnabled] = useState(Boolean(props.initialIsEnabled)); + const [discoveryRuleList, setDiscoveryRuleList] = + useState( + initialDiscoveryRules.map((discoveryRule) => ({ + id: generateId(), + discoveryRule, + })) + ); + const [editDiscoveryRuleId, setEditDiscoveryRuleId] = useState( + null + ); + const [version, setVersion] = useState(props.selectedVersion); + const [versions, setVersions] = useState(props.versions); + const [isValidVersion, setIsValidVersion] = useState( + validateVersion(version) + ); + + const onToggleEnable = useCallback(() => { + const nextIsEnabled = !isEnabled; + setIsEnabled(nextIsEnabled); + onChange({ + enabled: nextIsEnabled, + discoveryRules: nextIsEnabled + ? discoveryRuleList.map(({ discoveryRule }) => discoveryRule) + : [], + version: nextIsEnabled ? version : null, + }); + }, [isEnabled, onChange, discoveryRuleList, version]); + + const onDelete = useCallback( + (discoveryRuleId: string) => { + const filteredDiscoveryRuleList = discoveryRuleList.filter( + ({ id }) => id !== discoveryRuleId + ); + setDiscoveryRuleList(filteredDiscoveryRuleList); + onChange({ + enabled: isEnabled, + discoveryRules: filteredDiscoveryRuleList.map( + ({ discoveryRule }) => discoveryRule + ), + version, + }); + }, + [isEnabled, discoveryRuleList, onChange, version] + ); + + const onEdit = useCallback( + (discoveryRuleId: string) => { + const editingDiscoveryRule = discoveryRuleList.find( + ({ id }) => id === discoveryRuleId + ); + if (editingDiscoveryRule) { + const { + discoveryRule: { operation, type, probe }, + } = editingDiscoveryRule; + setStagedOperationText(operation); + setStagedTypeText(type); + setStagedProbeText(probe); + setEditDiscoveryRuleId(discoveryRuleId); + } + }, + [discoveryRuleList] + ); + + const [stagedOperationText, setStagedOperationText] = useState(''); + const [stagedTypeText, setStagedTypeText] = useState(''); + const [stagedProbeText, setStagedProbeText] = useState(''); + + const onChangeOperation = useCallback( + (operationText: string) => { + setStagedOperationText(operationText); + const selectedOperationTypes = props.operationTypes.find( + ({ operation }) => operationText === operation.value + ); + const selectedTypeAvailable = selectedOperationTypes?.types.some( + ({ value }) => stagedTypeText === value + ); + if (!selectedTypeAvailable) { + setStagedTypeText(selectedOperationTypes?.types[0].value ?? ''); + } + }, + [props.operationTypes, stagedTypeText] + ); + + const onChangeType = useCallback((operationText: string) => { + setStagedTypeText(operationText); + if (operationText === DISCOVERY_RULE_TYPE_ALL) { + setStagedProbeText(''); + } + }, []); + + const onChangeProbe = useCallback((operationText: string) => { + setStagedProbeText(operationText); + }, []); + + const onCancel = useCallback(() => { + if (editDiscoveryRuleId === STAGED_DISCOVERY_RULE_ID) { + onDelete(STAGED_DISCOVERY_RULE_ID); + } + setEditDiscoveryRuleId(null); + }, [editDiscoveryRuleId, onDelete]); + + const onSubmit = useCallback(() => { + const editDiscoveryRuleIndex = discoveryRuleList.findIndex( + ({ id }) => id === editDiscoveryRuleId + ); + const editDiscoveryRule = discoveryRuleList[editDiscoveryRuleIndex]; + const nextDiscoveryRuleList = [ + ...discoveryRuleList.slice(0, editDiscoveryRuleIndex), + { + id: + editDiscoveryRule.id === STAGED_DISCOVERY_RULE_ID + ? generateId() + : editDiscoveryRule.id, + discoveryRule: { + operation: stagedOperationText, + type: stagedTypeText, + probe: stagedProbeText, + }, + }, + ...discoveryRuleList.slice(editDiscoveryRuleIndex + 1), + ]; + setDiscoveryRuleList(nextDiscoveryRuleList); + setEditDiscoveryRuleId(null); + onChange({ + enabled: isEnabled, + discoveryRules: nextDiscoveryRuleList.map( + ({ discoveryRule }) => discoveryRule + ), + version, + }); + }, [ + isEnabled, + editDiscoveryRuleId, + stagedOperationText, + stagedTypeText, + stagedProbeText, + discoveryRuleList, + onChange, + version, + ]); + + const onAddRule = useCallback(() => { + const firstOperationType = props.operationTypes[0]; + const operationText = firstOperationType.operation.value; + const typeText = firstOperationType.types[0].value; + const valueText = ''; + setStagedOperationText(operationText); + setStagedTypeText(typeText); + setStagedProbeText(valueText); + const nextDiscoveryRuleList = [ + { + id: STAGED_DISCOVERY_RULE_ID, + discoveryRule: { + operation: operationText, + type: typeText, + probe: valueText, + }, + }, + ...discoveryRuleList, + ]; + setDiscoveryRuleList(nextDiscoveryRuleList); + setEditDiscoveryRuleId(STAGED_DISCOVERY_RULE_ID); + }, [discoveryRuleList, props.operationTypes]); + + const onDragEnd = useCallback( + ({ source, destination }: DropResult) => { + if (source && destination) { + const nextDiscoveryRuleList = euiDragDropReorder( + discoveryRuleList, + source.index, + destination.index + ); + setDiscoveryRuleList(nextDiscoveryRuleList); + onChange({ + enabled: isEnabled, + discoveryRules: nextDiscoveryRuleList.map( + ({ discoveryRule }) => discoveryRule + ), + version, + }); + } + }, + [isEnabled, discoveryRuleList, onChange, version] + ); + + function onChangeVersion(nextVersion?: string) { + if (!nextVersion) { + return; + } + setVersion(nextVersion); + onChange({ + enabled: isEnabled, + discoveryRules: isEnabled + ? discoveryRuleList.map(({ discoveryRule }) => discoveryRule) + : [], + version: nextVersion, + }); + } + + function onCreateNewVersion( + newVersion: string, + flattenedOptions: Array> + ) { + const normalizedNewVersion = newVersion.trim().toLowerCase(); + const isNextVersionValid = validateVersion(normalizedNewVersion); + setIsValidVersion(isNextVersionValid); + if (!normalizedNewVersion || !isNextVersionValid) { + return; + } + + // Create the option if it doesn't exist. + if ( + flattenedOptions.findIndex( + (option) => option.label.trim().toLowerCase() === normalizedNewVersion + ) === -1 + ) { + setVersions([...versions, newVersion]); + } + + onChangeVersion(newVersion); + } + + return ( + { + const nextVersion: string | undefined = selectedVersions[0]?.label; + const isNextVersionValid = validateVersion(nextVersion); + setIsValidVersion(isNextVersionValid); + onChangeVersion(nextVersion); + }} + onCreateNewVersion={onCreateNewVersion} + isValidVersion={isValidVersion} + /> + ); +} + +const generateId = htmlIdGenerator(); diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.stories.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.stories.tsx new file mode 100644 index 0000000000000..12f6705284ff9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.stories.tsx @@ -0,0 +1,484 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, Story } from '@storybook/react'; +import React, { useState } from 'react'; +import { RuntimeAttachment } from '.'; +import { JavaRuntimeAttachment } from './supported_agents/java_runtime_attachment'; + +const stories: Meta<{}> = { + title: 'fleet/Runtime agent attachment', + component: RuntimeAttachment, + decorators: [ + (StoryComponent) => { + return ( +
+ +
+ ); + }, + ], +}; +export default stories; + +const excludeOptions = [ + { value: 'main', label: 'main class / jar name' }, + { value: 'vmarg', label: 'vmarg' }, + { value: 'user', label: 'user' }, +]; +const includeOptions = [{ value: 'all', label: 'All' }, ...excludeOptions]; + +const versions = ['1.27.1', '1.27.0', '1.26.0', '1.25.0']; + +export const RuntimeAttachmentExample: Story = () => { + const [runtimeAttachmentSettings, setRuntimeAttachmentSettings] = useState( + {} + ); + return ( + <> + { + setRuntimeAttachmentSettings(settings); + }} + toggleDescription="Attach the Java agent to running and starting Java applications." + discoveryRulesDescription="For every running JVM, the discovery rules are evaluated in the order they are provided. The first matching rule determines the outcome. Learn more in the docs" + showUnsavedWarning={true} + initialIsEnabled={true} + initialDiscoveryRules={[ + { + operation: 'include', + type: 'main', + probe: 'java-opbeans-10010', + }, + { + operation: 'exclude', + type: 'vmarg', + probe: '10948653898867', + }, + ]} + versions={versions} + selectedVersion={versions[0]} + /> +
+
{JSON.stringify(runtimeAttachmentSettings, null, 4)}
+ + ); +}; + +export const JavaRuntimeAttachmentExample: Story = () => { + return ( + {}} + /> + ); +}; + +const policy = { + id: 'cc380ec5-d84e-40e1-885a-d706edbdc968', + version: 'WzM0MzA2LDJd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: 'policy-elastic-agent-on-cloud', + enabled: true, + output_id: '', + inputs: [ + { + type: 'apm', + policy_template: 'apmserver', + enabled: true, + streams: [], + vars: { + host: { + value: 'localhost:8200', + type: 'text', + }, + url: { + value: 'http://localhost:8200', + type: 'text', + }, + secret_token: { + type: 'text', + }, + api_key_enabled: { + value: false, + type: 'bool', + }, + enable_rum: { + value: true, + type: 'bool', + }, + anonymous_enabled: { + value: true, + type: 'bool', + }, + anonymous_allow_agent: { + value: ['rum-js', 'js-base', 'iOS/swift'], + type: 'text', + }, + anonymous_allow_service: { + value: [], + type: 'text', + }, + anonymous_rate_limit_event_limit: { + value: 10, + type: 'integer', + }, + anonymous_rate_limit_ip_limit: { + value: 10000, + type: 'integer', + }, + default_service_environment: { + type: 'text', + }, + rum_allow_origins: { + value: ['"*"'], + type: 'text', + }, + rum_allow_headers: { + value: [], + type: 'text', + }, + rum_response_headers: { + type: 'yaml', + }, + rum_library_pattern: { + value: '"node_modules|bower_components|~"', + type: 'text', + }, + rum_exclude_from_grouping: { + value: '"^/webpack"', + type: 'text', + }, + api_key_limit: { + value: 100, + type: 'integer', + }, + max_event_bytes: { + value: 307200, + type: 'integer', + }, + capture_personal_data: { + value: true, + type: 'bool', + }, + max_header_bytes: { + value: 1048576, + type: 'integer', + }, + idle_timeout: { + value: '45s', + type: 'text', + }, + read_timeout: { + value: '3600s', + type: 'text', + }, + shutdown_timeout: { + value: '30s', + type: 'text', + }, + write_timeout: { + value: '30s', + type: 'text', + }, + max_connections: { + value: 0, + type: 'integer', + }, + response_headers: { + type: 'yaml', + }, + expvar_enabled: { + value: false, + type: 'bool', + }, + tls_enabled: { + value: false, + type: 'bool', + }, + tls_certificate: { + type: 'text', + }, + tls_key: { + type: 'text', + }, + tls_supported_protocols: { + value: ['TLSv1.0', 'TLSv1.1', 'TLSv1.2'], + type: 'text', + }, + tls_cipher_suites: { + value: [], + type: 'text', + }, + tls_curve_types: { + value: [], + type: 'text', + }, + tail_sampling_policies: { + type: 'yaml', + }, + tail_sampling_interval: { + type: 'text', + }, + }, + config: { + 'apm-server': { + value: { + rum: { + source_mapping: { + metadata: [], + }, + }, + agent_config: [], + }, + }, + }, + compiled_input: { + 'apm-server': { + auth: { + anonymous: { + allow_agent: ['rum-js', 'js-base', 'iOS/swift'], + allow_service: null, + enabled: true, + rate_limit: { + event_limit: 10, + ip_limit: 10000, + }, + }, + api_key: { + enabled: false, + limit: 100, + }, + secret_token: null, + }, + capture_personal_data: true, + idle_timeout: '45s', + default_service_environment: null, + 'expvar.enabled': false, + host: 'localhost:8200', + max_connections: 0, + max_event_size: 307200, + max_header_size: 1048576, + read_timeout: '3600s', + response_headers: null, + rum: { + allow_headers: null, + allow_origins: ['*'], + enabled: true, + exclude_from_grouping: '^/webpack', + library_pattern: 'node_modules|bower_components|~', + response_headers: null, + }, + shutdown_timeout: '30s', + write_timeout: '30s', + }, + }, + }, + ], + package: { + name: 'apm', + title: 'Elastic APM', + version: '7.16.0', + }, + elasticsearch: { + privileges: { + cluster: ['cluster:monitor/main'], + }, + }, + revision: 1, + created_at: '2021-11-18T02:14:55.758Z', + created_by: 'admin', + updated_at: '2021-11-18T02:14:55.758Z', + updated_by: 'admin', +}; + +const newPolicy = { + version: 'WzM0MzA2LDJd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: 'policy-elastic-agent-on-cloud', + enabled: true, + output_id: '', + package: { + name: 'apm', + title: 'Elastic APM', + version: '8.0.0-dev2', + }, + elasticsearch: { + privileges: { + cluster: ['cluster:monitor/main'], + }, + }, + inputs: [ + { + type: 'apm', + policy_template: 'apmserver', + enabled: true, + vars: { + host: { + value: 'localhost:8200', + type: 'text', + }, + url: { + value: 'http://localhost:8200', + type: 'text', + }, + secret_token: { + type: 'text', + }, + api_key_enabled: { + value: false, + type: 'bool', + }, + enable_rum: { + value: true, + type: 'bool', + }, + anonymous_enabled: { + value: true, + type: 'bool', + }, + anonymous_allow_agent: { + value: ['rum-js', 'js-base', 'iOS/swift'], + type: 'text', + }, + anonymous_allow_service: { + value: [], + type: 'text', + }, + anonymous_rate_limit_event_limit: { + value: 10, + type: 'integer', + }, + anonymous_rate_limit_ip_limit: { + value: 10000, + type: 'integer', + }, + default_service_environment: { + type: 'text', + }, + rum_allow_origins: { + value: ['"*"'], + type: 'text', + }, + rum_allow_headers: { + value: [], + type: 'text', + }, + rum_response_headers: { + type: 'yaml', + }, + rum_library_pattern: { + value: '"node_modules|bower_components|~"', + type: 'text', + }, + rum_exclude_from_grouping: { + value: '"^/webpack"', + type: 'text', + }, + api_key_limit: { + value: 100, + type: 'integer', + }, + max_event_bytes: { + value: 307200, + type: 'integer', + }, + capture_personal_data: { + value: true, + type: 'bool', + }, + max_header_bytes: { + value: 1048576, + type: 'integer', + }, + idle_timeout: { + value: '45s', + type: 'text', + }, + read_timeout: { + value: '3600s', + type: 'text', + }, + shutdown_timeout: { + value: '30s', + type: 'text', + }, + write_timeout: { + value: '30s', + type: 'text', + }, + max_connections: { + value: 0, + type: 'integer', + }, + response_headers: { + type: 'yaml', + }, + expvar_enabled: { + value: false, + type: 'bool', + }, + tls_enabled: { + value: false, + type: 'bool', + }, + tls_certificate: { + type: 'text', + }, + tls_key: { + type: 'text', + }, + tls_supported_protocols: { + value: ['TLSv1.0', 'TLSv1.1', 'TLSv1.2'], + type: 'text', + }, + tls_cipher_suites: { + value: [], + type: 'text', + }, + tls_curve_types: { + value: [], + type: 'text', + }, + tail_sampling_policies: { + type: 'yaml', + }, + tail_sampling_interval: { + type: 'text', + }, + }, + config: { + 'apm-server': { + value: { + rum: { + source_mapping: { + metadata: [], + }, + }, + agent_config: [], + }, + }, + }, + streams: [], + }, + ], +}; diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.tsx new file mode 100644 index 0000000000000..3592eb4f04745 --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/runtime_attachment.tsx @@ -0,0 +1,235 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiCallOut, + EuiSpacer, + EuiSwitch, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiDragDropContext, + EuiDroppable, + EuiDraggable, + EuiIcon, + DropResult, + EuiComboBox, + EuiComboBoxProps, + EuiFormRow, +} from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { i18n } from '@kbn/i18n'; +import { DiscoveryRule } from './discovery_rule'; +import { DefaultDiscoveryRule } from './default_discovery_rule'; +import { EditDiscoveryRule } from './edit_discovery_rule'; +import { IDiscoveryRuleList, Operation } from '.'; + +interface Props { + isEnabled: boolean; + onToggleEnable: () => void; + discoveryRuleList: IDiscoveryRuleList; + setDiscoveryRuleList: (discoveryRuleItems: IDiscoveryRuleList) => void; + onDelete: (discoveryItemId: string) => void; + editDiscoveryRuleId: null | string; + onEdit: (discoveryItemId: string) => void; + onChangeOperation: (operationText: string) => void; + stagedOperationText: string; + onChangeType: (typeText: string) => void; + stagedTypeText: string; + onChangeProbe: (probeText: string) => void; + stagedProbeText: string; + onCancel: () => void; + onSubmit: () => void; + onAddRule: () => void; + operationTypes: Operation[]; + toggleDescription: ReactNode; + discoveryRulesDescription: ReactNode; + showUnsavedWarning?: boolean; + onDragEnd: (dropResult: DropResult) => void; + selectedVersion: string; + versions: string[]; + onChangeVersion: EuiComboBoxProps['onChange']; + onCreateNewVersion: EuiComboBoxProps['onCreateOption']; + isValidVersion: boolean; +} + +export function RuntimeAttachment({ + isEnabled, + onToggleEnable, + discoveryRuleList, + setDiscoveryRuleList, + onDelete, + editDiscoveryRuleId, + onEdit, + onChangeOperation, + stagedOperationText, + onChangeType, + stagedTypeText, + onChangeProbe, + stagedProbeText, + onCancel, + onSubmit, + onAddRule, + operationTypes, + toggleDescription, + discoveryRulesDescription, + showUnsavedWarning, + onDragEnd, + selectedVersion, + versions, + onChangeVersion, + onCreateNewVersion, + isValidVersion, +}: Props) { + return ( +
+ {showUnsavedWarning && ( + <> + + + + )} + + + + + +

{toggleDescription}

+
+
+ {isEnabled && versions && ( + + + ({ label: _version }))} + onChange={onChangeVersion} + onCreateOption={onCreateNewVersion} + singleSelection + isClearable={false} + /> + + + )} +
+ {isEnabled && ( + <> + + +

+ {i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.discoveryRules', + { defaultMessage: 'Discovery rules' } + )} +

+
+ + + + + + + +

{discoveryRulesDescription}

+
+
+ + + {i18n.translate( + 'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.addRule', + { defaultMessage: 'Add rule' } + )} + + +
+ + + + {discoveryRuleList.map(({ discoveryRule, id }, idx) => ( + + {(provided) => + id === editDiscoveryRuleId ? ( + + ) : ( + + ) + } + + ))} + + + + + )} + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/supported_agents/java_runtime_attachment.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/supported_agents/java_runtime_attachment.tsx new file mode 100644 index 0000000000000..2284315d4a6ba --- /dev/null +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/runtime_attachment/supported_agents/java_runtime_attachment.tsx @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import yaml from 'js-yaml'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useState, useMemo } from 'react'; +import { + RuntimeAttachment, + RuntimeAttachmentSettings, + IDiscoveryRule, +} from '..'; +import type { + NewPackagePolicy, + PackagePolicy, + PackagePolicyEditExtensionComponentProps, +} from '../../../apm_policy_form/typings'; + +interface Props { + policy: PackagePolicy; + newPolicy: NewPackagePolicy; + onChange: PackagePolicyEditExtensionComponentProps['onChange']; +} + +const excludeOptions = [ + { + value: 'main', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.main', + { defaultMessage: 'main' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.mainDescription', + { + defaultMessage: + 'A regular expression of fully qualified main class names or paths to JARs of applications the java agent should be attached to. Performs a partial match so that foo matches /bin/foo.jar.', + } + ), + }, + { + value: 'vmarg', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.vmarg', + { defaultMessage: 'vmarg' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.vmargDescription', + { + defaultMessage: + 'A regular expression matched against the arguments passed to the JVM, such as system properties. Performs a partial match so that attach=true matches the system property -Dattach=true.', + } + ), + }, + { + value: 'user', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.user', + { defaultMessage: 'user' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.userDescription', + { + defaultMessage: + 'A username that is matched against the operating system user that runs the JVM.', + } + ), + }, +]; +const includeOptions = [ + { + value: 'all', + label: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.include.options.all', + { defaultMessage: 'All' } + ), + description: i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.operationType.include.options.allDescription', + { defaultMessage: 'Includes all JVMs for attachment.' } + ), + }, + ...excludeOptions, +]; + +const versions = [ + '1.27.1', + '1.27.0', + '1.26.0', + '1.25.0', + '1.24.0', + '1.23.0', + '1.22.0', + '1.21.0', + '1.20.0', + '1.19.0', + '1.18.1', + '1.18.0', + '1.18.0.RC1', + '1.17.0', + '1.16.0', + '1.15.0', + '1.14.0', + '1.13.0', + '1.12.0', + '1.11.0', + '1.10.0', + '1.9.0', + '1.8.0', + '1.7.0', + '1.6.1', + '1.6.0', + '1.5.0', + '1.4.0', + '1.3.0', + '1.2.0', +]; + +function getApmVars(newPolicy: NewPackagePolicy) { + return newPolicy.inputs.find(({ type }) => type === 'apm')?.vars; +} + +export function JavaRuntimeAttachment({ newPolicy, onChange }: Props) { + const [isDirty, setIsDirty] = useState(false); + const onChangePolicy = useCallback( + (runtimeAttachmentSettings: RuntimeAttachmentSettings) => { + const apmInputIdx = newPolicy.inputs.findIndex( + ({ type }) => type === 'apm' + ); + onChange({ + isValid: true, + updatedPolicy: { + ...newPolicy, + inputs: [ + ...newPolicy.inputs.slice(0, apmInputIdx), + { + ...newPolicy.inputs[apmInputIdx], + vars: { + ...newPolicy.inputs[apmInputIdx].vars, + java_attacher_enabled: { + value: runtimeAttachmentSettings.enabled, + type: 'bool', + }, + java_attacher_discovery_rules: { + type: 'yaml', + value: encodeDiscoveryRulesYaml( + runtimeAttachmentSettings.discoveryRules + ), + }, + java_attacher_agent_version: { + type: 'text', + value: runtimeAttachmentSettings.version, + }, + }, + }, + ...newPolicy.inputs.slice(apmInputIdx + 1), + ], + }, + }); + setIsDirty(true); + }, + [newPolicy, onChange] + ); + + const apmVars = useMemo(() => getApmVars(newPolicy), [newPolicy]); + + return ( + + {i18n.translate( + 'xpack.apm.fleetIntegration.javaRuntime.discoveryRulesDescription.docLink', + { defaultMessage: 'docs' } + )} + + ), + }} + /> + } + showUnsavedWarning={isDirty} + initialIsEnabled={apmVars?.java_attacher_enabled?.value} + initialDiscoveryRules={decodeDiscoveryRulesYaml( + apmVars?.java_attacher_discovery_rules?.value ?? '[]\n', + [initialDiscoveryRule] + )} + selectedVersion={ + apmVars?.java_attacher_agent_version?.value || versions[0] + } + versions={versions} + /> + ); +} + +const initialDiscoveryRule = { + operation: 'include', + type: 'vmarg', + probe: 'elastic.apm.attach=true', +}; + +type DiscoveryRulesParsedYaml = Array<{ [operationType: string]: string }>; + +function decodeDiscoveryRulesYaml( + discoveryRulesYaml: string, + defaultDiscoveryRules: IDiscoveryRule[] = [] +): IDiscoveryRule[] { + try { + const parsedYaml: DiscoveryRulesParsedYaml = + yaml.load(discoveryRulesYaml) ?? []; + + if (parsedYaml.length === 0) { + return defaultDiscoveryRules; + } + + // transform into array of discovery rules + return parsedYaml.map((discoveryRuleMap) => { + const [operationType, probe] = Object.entries(discoveryRuleMap)[0]; + return { + operation: operationType.split('-')[0], + type: operationType.split('-')[1], + probe, + }; + }); + } catch (error) { + return defaultDiscoveryRules; + } +} + +function encodeDiscoveryRulesYaml(discoveryRules: IDiscoveryRule[]): string { + // transform into list of key,value objects for expected yaml result + const mappedDiscoveryRules: DiscoveryRulesParsedYaml = discoveryRules.map( + ({ operation, type, probe }) => ({ + [`${operation}-${type}`]: probe, + }) + ); + return yaml.dump(mappedDiscoveryRules); +} diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx index ec8366dfb36b4..229f34f7857ad 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx @@ -18,11 +18,11 @@ import { getLegacyApmHref } from '../../shared/Links/apm/APMLink'; type Tab = NonNullable[0] & { key: | 'agent-configurations' + | 'agent-keys' | 'anomaly-detection' | 'apm-indices' | 'customize-ui' - | 'schema' - | 'agent-keys'; + | 'schema'; hidden?: boolean; }; @@ -76,6 +76,17 @@ function getTabs({ search, }), }, + { + key: 'agent-keys', + label: i18n.translate('xpack.apm.settings.agentKeys', { + defaultMessage: 'Agent Keys', + }), + href: getLegacyApmHref({ + basePath, + path: `/settings/agent-keys`, + search, + }), + }, { key: 'anomaly-detection', label: i18n.translate('xpack.apm.settings.anomalyDetection', { @@ -117,17 +128,6 @@ function getTabs({ }), href: getLegacyApmHref({ basePath, path: `/settings/schema`, search }), }, - { - key: 'agent-keys', - label: i18n.translate('xpack.apm.settings.agentKeys', { - defaultMessage: 'Agent Keys', - }), - href: getLegacyApmHref({ - basePath, - path: `/settings/agent-keys`, - search, - }), - }, ]; return tabs diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx index eb7b531121753..4e2a7f477b666 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx @@ -7,47 +7,17 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; -import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { TimePickerRefreshInterval } from '../../DatePicker/typings'; +import { useMlManageJobsHref } from '../../../../hooks/use_ml_manage_jobs_href'; interface Props { children?: React.ReactNode; external?: boolean; + jobId?: string; } -export function MLManageJobsLink({ children, external }: Props) { - const { - core, - plugins: { ml }, - } = useApmPluginContext(); - - const { urlParams } = useLegacyUrlParams(); - - const timePickerRefreshIntervalDefaults = - core.uiSettings.get( - UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS - ); - - const { - // hardcoding a custom default of 1 hour since the default kibana timerange of 15 minutes is shorter than the ML interval - rangeFrom = 'now-1h', - rangeTo = 'now', - refreshInterval = timePickerRefreshIntervalDefaults.value, - refreshPaused = timePickerRefreshIntervalDefaults.pause, - } = urlParams; - - const mlADLink = useMlHref(ml, core.http.basePath.get(), { - page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, - pageState: { - groupIds: ['apm'], - globalState: { - time: { from: rangeFrom, to: rangeTo }, - refreshInterval: { pause: refreshPaused, value: refreshInterval }, - }, - }, +export function MLManageJobsLink({ children, external, jobId }: Props) { + const mlADLink = useMlManageJobsHref({ + jobId, }); return ( diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx index 0520cfa39a743..e47c4853827de 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx @@ -5,28 +5,55 @@ * 2.0. */ +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react'; -import { MissingJobsAlert } from './anomaly_detection_setup_link'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { ApmMlJob } from '../../../../common/anomaly_detection/apm_ml_job'; +import { getAnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import * as hooks from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; async function renderTooltipAnchor({ jobs, environment, }: { - jobs: Array<{ job_id: string; environment: string }>; + jobs: ApmMlJob[]; environment?: string; }) { // mock api response jest.spyOn(hooks, 'useAnomalyDetectionJobsContext').mockReturnValue({ - anomalyDetectionJobsData: { jobs, hasLegacyJobs: false }, + anomalyDetectionJobsData: { + jobs, + hasLegacyJobs: jobs.some((job) => job.version <= 2), + }, anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, anomalyDetectionJobsRefetch: () => {}, + anomalyDetectionSetupState: getAnomalyDetectionSetupState({ + environment: environment ?? ENVIRONMENT_ALL.value, + fetchStatus: FETCH_STATUS.SUCCESS, + isAuthorized: true, + jobs, + }), + }); + + const history = createMemoryHistory({ + initialEntries: [ + `/services?environment=${ + environment || ENVIRONMENT_ALL.value + }&rangeFrom=now-15m&rangeTo=now`, + ], }); const { baseElement, container } = render( - + + + + + ); // hover tooltip anchor if it exists @@ -65,7 +92,13 @@ describe('MissingJobsAlert', () => { describe('when no jobs exists for the selected environment', () => { it('shows a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'production', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'production', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], environment: 'staging', }); @@ -79,7 +112,13 @@ describe('MissingJobsAlert', () => { describe('when a job exists for the selected environment', () => { it('does not show a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'production', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'production', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], environment: 'production', }); @@ -91,7 +130,13 @@ describe('MissingJobsAlert', () => { describe('when at least one job exists and no environment is selected', () => { it('does not show a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'production', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'production', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], }); expect(toolTipAnchor).not.toBeInTheDocument(); @@ -102,7 +147,54 @@ describe('MissingJobsAlert', () => { describe('when at least one job exists and all environments are selected', () => { it('does not show a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'ENVIRONMENT_ALL', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], + }); + + expect(toolTipAnchor).not.toBeInTheDocument(); + expect(toolTipText).toBe(undefined); + }); + }); + + describe('when at least one legacy job exists', () => { + it('displays a nudge to upgrade', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [ + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id', + version: 2, + } as ApmMlJob, + ], + }); + + expect(toolTipAnchor).toBeInTheDocument(); + expect(toolTipText).toBe( + 'Updates available for existing anomaly detection jobs.' + ); + }); + }); + + describe('when both legacy and modern jobs exist', () => { + it('does not show a tooltip', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [ + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id', + version: 2, + } as ApmMlJob, + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id_2', + version: 3, + } as ApmMlJob, + ], }); expect(toolTipAnchor).not.toBeInTheDocument(); diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx index 4891ca896076a..e1bda5475acc4 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx @@ -5,32 +5,22 @@ * 2.0. */ -import { - EuiHeaderLink, - EuiIcon, - EuiLoadingSpinner, - EuiToolTip, -} from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { IconType } from '@elastic/eui'; +import { EuiHeaderLink, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { ENVIRONMENT_ALL, getEnvironmentLabel, } from '../../../../common/environment_filter_values'; import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { useLicenseContext } from '../../../context/license/use_license_context'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { getLegacyApmHref } from '../Links/apm/APMLink'; -export type AnomalyDetectionApiResponse = - APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>; - -const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; - export function AnomalyDetectionSetupLink() { const { query } = useApmParams('/*'); @@ -38,71 +28,86 @@ export function AnomalyDetectionSetupLink() { ('environment' in query && query.environment) || ENVIRONMENT_ALL.value; const { core } = useApmPluginContext(); - const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; - const license = useLicenseContext(); - const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); + const { basePath } = core.http; const theme = useTheme(); - return ( + const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext(); + + let tooltipText: string = ''; + let color: 'warning' | 'text' | 'success' | 'danger' = 'text'; + let icon: IconType | undefined; + + if (anomalyDetectionSetupState === AnomalyDetectionSetupState.Failure) { + color = 'warning'; + tooltipText = i18n.translate( + 'xpack.apm.anomalyDetectionSetup.jobFetchFailureText', + { + defaultMessage: 'Could not determine state of anomaly detection setup.', + } + ); + icon = 'alert'; + } else if ( + anomalyDetectionSetupState === AnomalyDetectionSetupState.NoJobs || + anomalyDetectionSetupState === + AnomalyDetectionSetupState.NoJobsForEnvironment + ) { + color = 'warning'; + tooltipText = getNoJobsMessage(anomalyDetectionSetupState, environment); + icon = 'alert'; + } else if ( + anomalyDetectionSetupState === AnomalyDetectionSetupState.UpgradeableJobs + ) { + color = 'success'; + tooltipText = i18n.translate( + 'xpack.apm.anomalyDetectionSetup.upgradeableJobsText', + { + defaultMessage: + 'Updates available for existing anomaly detection jobs.', + } + ); + icon = 'wrench'; + } + + let pre: React.ReactElement | null = null; + + if (anomalyDetectionSetupState === AnomalyDetectionSetupState.Loading) { + pre = ; + } else if (icon) { + pre = ; + } + + const element = ( - {canGetJobs && hasValidLicense ? ( - - ) : ( - - )} + {pre} {ANOMALY_DETECTION_LINK_LABEL} ); -} - -export function MissingJobsAlert({ environment }: { environment?: string }) { - const { - anomalyDetectionJobsData = DEFAULT_DATA, - anomalyDetectionJobsStatus, - } = useAnomalyDetectionJobsContext(); - const defaultIcon = ; - - if (anomalyDetectionJobsStatus === FETCH_STATUS.LOADING) { - return ; - } - - if (anomalyDetectionJobsStatus !== FETCH_STATUS.SUCCESS) { - return defaultIcon; - } - - const isEnvironmentSelected = - environment && environment !== ENVIRONMENT_ALL.value; - - // there are jobs for at least one environment - if (!isEnvironmentSelected && anomalyDetectionJobsData.jobs.length > 0) { - return defaultIcon; - } - - // there are jobs for the selected environment - if ( - isEnvironmentSelected && - anomalyDetectionJobsData.jobs.some((job) => environment === job.environment) - ) { - return defaultIcon; - } - - return ( - - + const wrappedElement = tooltipText ? ( + + {element} + ) : ( + element ); + + return wrappedElement; } -function getTooltipText(environment?: string) { - if (!environment || environment === ENVIRONMENT_ALL.value) { +function getNoJobsMessage( + state: + | AnomalyDetectionSetupState.NoJobs + | AnomalyDetectionSetupState.NoJobsForEnvironment, + environment: string +) { + if (state === AnomalyDetectionSetupState.NoJobs) { return i18n.translate('xpack.apm.anomalyDetectionSetup.notEnabledText', { defaultMessage: `Anomaly detection is not yet enabled. Click to continue setup.`, }); diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx index 30ca3e79f6d7b..03ae13c06c613 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx @@ -44,6 +44,7 @@ interface Props { pagination?: boolean; isLoading?: boolean; error?: boolean; + tableLayout?: 'auto' | 'fixed'; } function defaultSortFn( @@ -70,6 +71,7 @@ function UnoptimizedManagedTable(props: Props) { pagination = true, isLoading = false, error = false, + tableLayout, } = props; const { @@ -141,6 +143,7 @@ function UnoptimizedManagedTable(props: Props) { // @ts-expect-error TS thinks pagination should be non-nullable, but it's not void; + onUpgradeClick?: () => any; + onCreateJobClick?: () => void; + isOnSettingsPage: boolean; + append?: React.ReactElement; +}) { + const [loading, setLoading] = useState(false); + + const mlManageJobsHref = useMlManageJobsHref(); + + let properties: + | { + primaryAction: React.ReactNode | undefined; + color: 'primary' | 'success' | 'danger' | 'warning'; + title: string; + icon: string; + text: string; + } + | undefined; + + const getLearnMoreLink = (color: 'primary' | 'success') => ( + + + {i18n.translate('xpack.apm.mlCallout.learnMoreButton', { + defaultMessage: `Learn more`, + })} + + + ); + + switch (anomalyDetectionSetupState) { + case AnomalyDetectionSetupState.NoJobs: + properties = { + title: i18n.translate('xpack.apm.mlCallout.noJobsCalloutTitle', { + defaultMessage: + 'Enable anomaly detection to add health status indicators to your services', + }), + text: i18n.translate('xpack.apm.mlCallout.noJobsCalloutText', { + defaultMessage: `Pinpoint anomalous transactions and see the health of upstream and downstream services with APM's anomaly detection integration. Get started in just a few minutes.`, + }), + icon: 'iInCircle', + color: 'primary', + primaryAction: isOnSettingsPage ? ( + { + onCreateJobClick?.(); + }} + > + {i18n.translate('xpack.apm.mlCallout.noJobsCalloutButtonText', { + defaultMessage: 'Create ML Job', + })} + + ) : ( + getLearnMoreLink('primary') + ), + }; + break; + + case AnomalyDetectionSetupState.UpgradeableJobs: + properties = { + title: i18n.translate( + 'xpack.apm.mlCallout.updateAvailableCalloutTitle', + { defaultMessage: 'Updates available' } + ), + text: i18n.translate('xpack.apm.mlCallout.updateAvailableCalloutText', { + defaultMessage: + 'We have updated the anomaly detection jobs that provide insights into degraded performance and added detectors for throughput and failed transaction rate. If you choose to upgrade, we will create the new jobs and close the existing legacy jobs. The data shown in the APM app will automatically switch to the new.', + }), + color: 'success', + icon: 'wrench', + primaryAction: isOnSettingsPage ? ( + { + setLoading(true); + Promise.resolve(onUpgradeClick?.()).finally(() => { + setLoading(false); + }); + }} + > + {i18n.translate( + 'xpack.apm.mlCallout.updateAvailableCalloutButtonText', + { + defaultMessage: 'Update jobs', + } + )} + + ) : ( + getLearnMoreLink('success') + ), + }; + break; + + case AnomalyDetectionSetupState.LegacyJobs: + properties = { + title: i18n.translate('xpack.apm.mlCallout.legacyJobsCalloutTitle', { + defaultMessage: 'Legacy ML jobs are no longer used in APM app', + }), + text: i18n.translate('xpack.apm.mlCallout.legacyJobsCalloutText', { + defaultMessage: + 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app', + }), + icon: 'iInCircle', + color: 'primary', + primaryAction: ( + + {i18n.translate( + 'xpack.apm.settings.anomaly_detection.legacy_jobs.button', + { defaultMessage: 'Review jobs' } + )} + + ), + }; + break; + } + + if (!properties) { + return null; + } + + const dismissable = !isOnSettingsPage; + + const hasAnyActions = properties.primaryAction || dismissable; + + const actions = hasAnyActions ? ( + + {properties.primaryAction && ( + {properties.primaryAction} + )} + {dismissable && ( + + + {i18n.translate('xpack.apm.mlCallout.dismissButton', { + defaultMessage: `Dismiss`, + })} + + + )} + + ) : null; + + return ( + +

{properties.text}

+ {actions} +
+ ); +} diff --git a/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx b/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx index bf9f2941fa2fb..3b9cea7b88998 100644 --- a/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx +++ b/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React, { createContext, ReactChild, useState } from 'react'; +import React, { createContext, ReactChild } from 'react'; +import { + AnomalyDetectionSetupState, + getAnomalyDetectionSetupState, +} from '../../../common/anomaly_detection/get_anomaly_detection_setup_state'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { useApmParams } from '../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; import { APIReturnType } from '../../services/rest/createCallApmApi'; +import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; +import { useLicenseContext } from '../license/use_license_context'; export interface AnomalyDetectionJobsContextValue { anomalyDetectionJobsData?: APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>; anomalyDetectionJobsStatus: FETCH_STATUS; anomalyDetectionJobsRefetch: () => void; + anomalyDetectionSetupState: AnomalyDetectionSetupState; } export const AnomalyDetectionJobsContext = createContext( @@ -24,24 +33,45 @@ export function AnomalyDetectionJobsContextProvider({ }: { children: ReactChild; }) { - const [fetchId, setFetchId] = useState(0); - const refetch = () => setFetchId((id) => id + 1); + const { core } = useApmPluginContext(); + const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; + const license = useLicenseContext(); + const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); - const { data, status } = useFetcher( - (callApmApi) => - callApmApi({ + const isAuthorized = !!(canGetJobs && hasValidLicense); + + const { data, status, refetch } = useFetcher( + (callApmApi) => { + if (!isAuthorized) { + return; + } + return callApmApi({ endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`, - }), - [fetchId], // eslint-disable-line react-hooks/exhaustive-deps + }); + }, + [isAuthorized], { showToastOnError: false } ); + const { query } = useApmParams('/*'); + + const environment = + ('environment' in query && query.environment) || ENVIRONMENT_ALL.value; + + const anomalyDetectionSetupState = getAnomalyDetectionSetupState({ + environment, + fetchStatus: status, + jobs: data?.jobs ?? [], + isAuthorized, + }); + return ( {children} diff --git a/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts b/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts new file mode 100644 index 0000000000000..cc187c6cf619a --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; +import { ML_PAGES, useMlHref } from '../../../ml/public'; +import { TimePickerRefreshInterval } from '../components/shared/DatePicker/typings'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; +import { useLegacyUrlParams } from '../context/url_params_context/use_url_params'; + +export function useMlManageJobsHref({ jobId }: { jobId?: string } = {}) { + const { + core, + plugins: { ml }, + } = useApmPluginContext(); + + const { urlParams } = useLegacyUrlParams(); + + const timePickerRefreshIntervalDefaults = + core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS + ); + + const { + // hardcoding a custom default of 1 hour since the default kibana timerange of 15 minutes is shorter than the ML interval + rangeFrom = 'now-1h', + rangeTo = 'now', + refreshInterval = timePickerRefreshIntervalDefaults.value, + refreshPaused = timePickerRefreshIntervalDefaults.pause, + } = urlParams; + + const mlADLink = useMlHref(ml, core.http.basePath.get(), { + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + groupIds: ['apm'], + jobId, + globalState: { + time: { from: rangeFrom, to: rangeTo }, + refreshInterval: { pause: refreshPaused, value: refreshInterval }, + }, + }, + }); + + return mlADLink; +} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 3a439df245609..d62cca4e07d45 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -54,6 +54,8 @@ import { getLazyApmAgentsTabExtension } from './components/fleet_integration/laz import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension'; import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; import { featureCatalogueEntry } from './featureCatalogueEntry'; +import type { SecurityPluginStart } from '../../security/public'; + export type ApmPluginSetup = ReturnType; export type ApmPluginStart = void; @@ -81,6 +83,7 @@ export interface ApmPluginStartDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; observability: ObservabilityPublicStart; fleet?: FleetStart; + security?: SecurityPluginStart; } const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts index 11deff82de572..6b00c5cdd9a2b 100644 --- a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { kibanaPackageJson } from '@kbn/dev-utils'; +import { kibanaPackageJson } from '@kbn/utils'; import { GetDeprecationsContext } from '../../../../../src/core/server'; import { CloudSetup } from '../../../cloud/server'; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 416a873bac0a9..958bfb672083a 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -17,6 +17,7 @@ import { APMPlugin } from './plugin'; // All options should be documented in the APM configuration settings: https://github.com/elastic/kibana/blob/main/docs/settings/apm-settings.asciidoc // and be included on cloud allow list unless there are specific reasons not to const configSchema = schema.object({ + autoCreateApmDataView: schema.boolean({ defaultValue: true }), serviceMapEnabled: schema.boolean({ defaultValue: true }), serviceMapFingerprintBucketSize: schema.number({ defaultValue: 100 }), serviceMapTraceIdBucketSize: schema.number({ defaultValue: 65 }), @@ -25,7 +26,6 @@ const configSchema = schema.object({ }), serviceMapTraceIdGlobalBucketSize: schema.number({ defaultValue: 6 }), serviceMapMaxTracesPerRequest: schema.number({ defaultValue: 50 }), - autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), @@ -59,7 +59,15 @@ const configSchema = schema.object({ // plugin config export const config: PluginConfigDescriptor = { - deprecations: ({ renameFromRoot, deprecateFromRoot, unusedFromRoot }) => [ + deprecations: ({ + rename, + renameFromRoot, + deprecateFromRoot, + unusedFromRoot, + }) => [ + rename('autocreateApmIndexPattern', 'autoCreateApmDataView', { + level: 'warning', + }), renameFromRoot( 'apm_oss.transactionIndices', 'xpack.apm.indices.transaction', diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts index 4eec3b39f3739..2720dbdecfe1c 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts @@ -4,12 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - MlJob, - QueryDslQueryContainer, -} from '@elastic/elasticsearch/lib/api/types'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { ApmMlJob } from '../../../common/anomaly_detection/apm_ml_job'; -export function apmMlJobsQuery(jobs: MlJob[]) { +export function apmMlJobsQuery(jobs: ApmMlJob[]) { if (!jobs.length) { throw new Error('At least one ML job should be given'); } @@ -17,7 +15,7 @@ export function apmMlJobsQuery(jobs: MlJob[]) { return [ { terms: { - job_id: jobs.map((job) => job.job_id), + job_id: jobs.map((job) => job.jobId), }, }, ] as QueryDslQueryContainer[]; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 7277a12c2bf14..d855adee4a9ba 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -15,6 +15,7 @@ import { METRICSET_NAME, PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; +import { Environment } from '../../../common/environment_rt'; import { ProcessorEvent } from '../../../common/processor_event'; import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; @@ -24,7 +25,7 @@ import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( setup: Setup, - environments: string[], + environments: Environment[], logger: Logger ) { const { ml, indices } = setup; @@ -33,13 +34,6 @@ export async function createAnomalyDetectionJobs( throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } - const mlCapabilities = await withApmSpan('get_ml_capabilities', () => - ml.mlSystem.mlCapabilities() - ); - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - const uniqueMlJobEnvs = await getUniqueMlJobEnvs(setup, environments, logger); if (uniqueMlJobEnvs.length === 0) { return []; @@ -56,6 +50,7 @@ export async function createAnomalyDetectionJobs( createAnomalyDetectionJob({ ml, environment, dataViewName }) ) ); + const jobResponses = responses.flatMap((response) => response.jobs); const failedJobs = jobResponses.filter(({ success }) => !success); @@ -116,12 +111,15 @@ async function createAnomalyDetectionJob({ async function getUniqueMlJobEnvs( setup: Setup, - environments: string[], + environments: Environment[], logger: Logger ) { // skip creation of duplicate ML jobs - const jobs = await getAnomalyDetectionJobs(setup, logger); - const existingMlJobEnvs = jobs.map(({ environment }) => environment); + const jobs = await getAnomalyDetectionJobs(setup); + const existingMlJobEnvs = jobs + .filter((job) => job.version === 3) + .map(({ environment }) => environment); + const requestedExistingMlJobEnvs = environments.filter((env) => existingMlJobEnvs.includes(env) ); diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 75b2e8289c7a8..9047ae9ed90d0 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -4,41 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { Logger } from 'kibana/server'; import Boom from '@hapi/boom'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { +export function getAnomalyDetectionJobs(setup: Setup) { const { ml } = setup; if (!ml) { throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } - return withApmSpan('get_anomaly_detection_jobs', async () => { - const mlCapabilities = await withApmSpan('get_ml_capabilities', () => - ml.mlSystem.mlCapabilities() - ); - - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - - const response = await getMlJobsWithAPMGroup(ml.anomalyDetectors); - return response.jobs - .filter( - (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 - ) - .map((job) => { - const environment = job.custom_settings?.job_tags?.environment ?? ''; - return { - job_id: job.job_id, - environment, - }; - }); - }); + return getMlJobsWithAPMGroup(ml.anomalyDetectors); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts index 77ffef9801a86..37279d3320585 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts @@ -46,9 +46,7 @@ export async function getAnomalyTimeseries({ end, }); - const { jobs: mlJobs } = await getMlJobsWithAPMGroup( - mlSetup.anomalyDetectors - ); + const mlJobs = await getMlJobsWithAPMGroup(mlSetup.anomalyDetectors); if (!mlJobs.length) { return []; @@ -148,7 +146,7 @@ export async function getAnomalyTimeseries({ } ); - const jobsById = keyBy(mlJobs, (job) => job.job_id); + const jobsById = keyBy(mlJobs, (job) => job.jobId); function divide(value: number | null, divider: number) { if (value === null) { @@ -176,9 +174,9 @@ export async function getAnomalyTimeseries({ jobId, type, serviceName: bucket.key.serviceName as string, - environment: job.custom_settings!.job_tags!.environment as string, + environment: job.environment, transactionType: bucket.key.transactionType as string, - version: Number(job.custom_settings!.job_tags!.apm_ml_version), + version: job.version, anomalies: bucket.timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key as number, y: diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts index bcea8f1ed6b26..1f989ba17fe7c 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts @@ -6,23 +6,63 @@ */ import { MlPluginSetup } from '../../../../ml/server'; +import { ApmMlJob } from '../../../common/anomaly_detection/apm_ml_job'; +import { Environment } from '../../../common/environment_rt'; import { withApmSpan } from '../../utils/with_apm_span'; import { APM_ML_JOB_GROUP } from './constants'; // returns ml jobs containing "apm" group // workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned + +function catch404(e: any) { + if (e.statusCode === 404) { + return []; + } + + throw e; +} + export function getMlJobsWithAPMGroup( anomalyDetectors: ReturnType -) { +): Promise { return withApmSpan('get_ml_jobs_with_apm_group', async () => { try { - return await anomalyDetectors.jobs(APM_ML_JOB_GROUP); - } catch (e) { - if (e.statusCode === 404) { - return { count: 0, jobs: [] }; - } + const [jobs, allJobStats, allDatafeedStats] = await Promise.all([ + anomalyDetectors + .jobs(APM_ML_JOB_GROUP) + .then((response) => response.jobs), + anomalyDetectors + .jobStats(APM_ML_JOB_GROUP) + .then((response) => response.jobs) + .catch(catch404), + anomalyDetectors + .datafeedStats(`datafeed-${APM_ML_JOB_GROUP}*`) + .then((response) => response.datafeeds) + .catch(catch404), + ]); + + return jobs.map((job): ApmMlJob => { + const jobStats = allJobStats.find( + (stats) => stats.job_id === job.job_id + ); - throw e; + const datafeedStats = allDatafeedStats.find( + (stats) => stats.datafeed_id === job.datafeed_config?.datafeed_id + ); + + return { + environment: String( + job.custom_settings?.job_tags?.environment + ) as Environment, + jobId: job.job_id, + jobState: jobStats?.state as ApmMlJob['jobState'], + version: Number(job.custom_settings?.job_tags?.apm_ml_version ?? 1), + datafeedId: datafeedStats?.datafeed_id, + datafeedState: datafeedStats?.state as ApmMlJob['datafeedState'], + }; + }); + } catch (e) { + return catch404(e) as ApmMlJob[]; } }); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts deleted file mode 100644 index c189d24efc23a..0000000000000 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { withApmSpan } from '../../utils/with_apm_span'; -import { Setup } from '../helpers/setup_request'; -import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; - -// Determine whether there are any legacy ml jobs. -// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction -export function hasLegacyJobs(setup: Setup) { - const { ml } = setup; - - if (!ml) { - throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); - } - - return withApmSpan('has_legacy_jobs', async () => { - const mlCapabilities = await withApmSpan('get_ml_capabilities', () => - ml.mlSystem.mlCapabilities() - ); - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - - const response = await getMlJobsWithAPMGroup(ml.anomalyDetectors); - return response.jobs.some( - (job) => - job.job_id.endsWith('high_mean_response_time') && - job.custom_settings?.created_by === 'ml-module-apm-transaction' - ); - }); -} diff --git a/x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts b/x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts new file mode 100644 index 0000000000000..02207dad32efb --- /dev/null +++ b/x-pack/plugins/apm/server/routes/agent_keys/create_agent_key.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { ApmPluginRequestHandlerContext } from '../typings'; +import { CreateApiKeyResponse } from '../../../common/agent_key_types'; + +const enum PrivilegeType { + SOURCEMAP = 'sourcemap:write', + EVENT = 'event:write', + AGENT_CONFIG = 'config_agent:read', +} + +interface SecurityHasPrivilegesResponse { + application: { + apm: { + '-': { + [PrivilegeType.SOURCEMAP]: boolean; + [PrivilegeType.EVENT]: boolean; + [PrivilegeType.AGENT_CONFIG]: boolean; + }; + }; + }; + has_all_requested: boolean; + username: string; +} + +export async function createAgentKey({ + context, + requestBody, +}: { + context: ApmPluginRequestHandlerContext; + requestBody: { + name: string; + sourcemap?: boolean; + event?: boolean; + agentConfig?: boolean; + }; +}) { + // Elasticsearch will allow a user without the right apm privileges to create API keys, but the keys won't validate + // check first whether the user has the right privileges, and bail out early if not + const { + body: { application, username, has_all_requested: hasRequiredPrivileges }, + } = await context.core.elasticsearch.client.asCurrentUser.security.hasPrivileges( + { + body: { + application: [ + { + application: 'apm', + privileges: [ + PrivilegeType.SOURCEMAP, + PrivilegeType.EVENT, + PrivilegeType.AGENT_CONFIG, + ], + resources: ['-'], + }, + ], + }, + } + ); + + if (!hasRequiredPrivileges) { + const missingPrivileges = Object.entries(application.apm['-']) + .filter((x) => !x[1]) + .map((x) => x[0]) + .join(', '); + const error = `${username} is missing the following requested privilege(s): ${missingPrivileges}.\ + You might try with the superuser, or add the APM application privileges to the role of the authenticated user, eg.: + PUT /_security/role/my_role { + ... + "applications": [{ + "application": "apm", + "privileges": ["sourcemap:write", "event:write", "config_agent:read"], + "resources": ["*"] + }], + ... + }`; + throw Boom.internal(error); + } + + const { name = 'apm-key', sourcemap, event, agentConfig } = requestBody; + + const privileges: PrivilegeType[] = []; + if (!sourcemap && !event && !agentConfig) { + privileges.push( + PrivilegeType.SOURCEMAP, + PrivilegeType.EVENT, + PrivilegeType.AGENT_CONFIG + ); + } + + if (sourcemap) { + privileges.push(PrivilegeType.SOURCEMAP); + } + + if (event) { + privileges.push(PrivilegeType.EVENT); + } + + if (agentConfig) { + privileges.push(PrivilegeType.AGENT_CONFIG); + } + + const body = { + name, + metadata: { + application: 'apm', + }, + role_descriptors: { + apm: { + cluster: [], + index: [], + applications: [ + { + application: 'apm', + privileges, + resources: ['*'], + }, + ], + }, + }, + }; + + const { body: agentKey } = + await context.core.elasticsearch.client.asCurrentUser.security.createApiKey( + { + body, + } + ); + + return { + agentKey, + }; +} diff --git a/x-pack/plugins/apm/server/routes/agent_keys/route.ts b/x-pack/plugins/apm/server/routes/agent_keys/route.ts index e5f40205b2912..44bbb22e703b5 100644 --- a/x-pack/plugins/apm/server/routes/agent_keys/route.ts +++ b/x-pack/plugins/apm/server/routes/agent_keys/route.ts @@ -8,11 +8,13 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; import * as t from 'io-ts'; +import { toBooleanRt } from '@kbn/io-ts-utils/to_boolean_rt'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { getAgentKeys } from './get_agent_keys'; import { getAgentKeysPrivileges } from './get_agent_keys_privileges'; import { invalidateAgentKey } from './invalidate_agent_key'; +import { createAgentKey } from './create_agent_key'; const agentKeysRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/agent_keys', @@ -74,10 +76,40 @@ const invalidateAgentKeyRoute = createApmServerRoute({ }, }); +const createAgentKeyRoute = createApmServerRoute({ + endpoint: 'POST /apm/agent_keys', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + body: t.intersection([ + t.partial({ + sourcemap: toBooleanRt, + event: toBooleanRt, + agentConfig: toBooleanRt, + }), + t.type({ + name: t.string, + }), + ]), + }), + handler: async (resources) => { + const { context, params } = resources; + + const { body: requestBody } = params; + + const agentKey = await createAgentKey({ + context, + requestBody, + }); + + return agentKey; + }, +}); + export const agentKeysRouteRepository = createApmServerRouteRepository() .add(agentKeysRoute) .add(agentKeysPrivilegesRoute) - .add(invalidateAgentKeyRoute); + .add(invalidateAgentKeyRoute) + .add(createAgentKeyRoute); const SECURITY_REQUIRED_MESSAGE = i18n.translate( 'xpack.apm.api.apiKeys.securityRequired', diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts index fa4125c54126d..889fe3c16596e 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -6,9 +6,10 @@ */ import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; import { ANOMALY_SEVERITY } from '../../../common/ml_constants'; -import { Job, MlPluginSetup } from '../../../../ml/server'; +import { MlPluginSetup } from '../../../../ml/server'; import * as GetServiceAnomalies from '../service_map/get_service_anomalies'; import { createRuleTypeMocks } from './test_utils'; +import { ApmMlJob } from '../../../common/anomaly_detection/apm_ml_job'; describe('Transaction duration anomaly alert', () => { afterEach(() => { @@ -65,14 +66,14 @@ describe('Transaction duration anomaly alert', () => { jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue( Promise.resolve([ { - job_id: '1', - custom_settings: { job_tags: { environment: 'development' } }, + jobId: '1', + environment: 'development', }, { - job_id: '2', - custom_settings: { job_tags: { environment: 'production' } }, + jobId: '2', + environment: 'production', }, - ] as unknown as Job[]) + ] as unknown as ApmMlJob[]) ); const { services, dependencies, executor } = createRuleTypeMocks(); @@ -118,14 +119,14 @@ describe('Transaction duration anomaly alert', () => { jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue( Promise.resolve([ { - job_id: '1', - custom_settings: { job_tags: { environment: 'development' } }, + jobId: '1', + environment: 'development', }, { - job_id: '2', - custom_settings: { job_tags: { environment: 'production' } }, + jobId: '2', + environment: 'production', }, - ] as unknown as Job[]) + ] as unknown as ApmMlJob[]) ); const { services, dependencies, executor, scheduleActions } = diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts index dead149cd7761..5216d485bc31e 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -126,7 +126,7 @@ export function registerTransactionDurationAnomalyAlertType({ return {}; } - const jobIds = mlJobs.map((job) => job.job_id); + const jobIds = mlJobs.map((job) => job.jobId); const anomalySearchParams = { body: { size: 0, @@ -190,7 +190,7 @@ export function registerTransactionDurationAnomalyAlertType({ .map((bucket) => { const latest = bucket.latest_score.top[0].metrics; - const job = mlJobs.find((j) => j.job_id === latest.job_id); + const job = mlJobs.find((j) => j.jobId === latest.job_id); if (!job) { logger.warn( @@ -202,7 +202,7 @@ export function registerTransactionDurationAnomalyAlertType({ return { serviceName: latest.partition_field_value as string, transactionType: latest.by_field_value as string, - environment: job.custom_settings!.job_tags!.environment, + environment: job.environment, score: latest.record_score as number, }; }) diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts index c936e626a5599..a41e3370c1063 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_boolean_field_stats.ts @@ -7,8 +7,6 @@ import { ElasticsearchClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStatsCommonRequestParams, @@ -25,7 +23,7 @@ export const getBooleanFieldStatsRequest = ( ): estypes.SearchRequest => { const query = getQueryWithParams({ params, termFilters }); - const { index, samplerShardSize } = params; + const { index } = params; const size = 0; const aggs: Aggs = { @@ -42,14 +40,13 @@ export const getBooleanFieldStatsRequest = ( const searchBody = { query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, + aggs, }; return { index, size, + track_total_hits: false, body: searchBody, }; }; @@ -67,19 +64,17 @@ export const fetchBooleanFieldStats = async ( ); const { body } = await esClient.search(request); const aggregations = body.aggregations as { - sample: { - sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; - sampled_values: estypes.AggregationsTermsAggregate; - }; + sampled_value_count: estypes.AggregationsFiltersBucketItemKeys; + sampled_values: estypes.AggregationsTermsAggregate; }; const stats: BooleanFieldStats = { fieldName: field.fieldName, - count: aggregations?.sample.sampled_value_count.doc_count ?? 0, + count: aggregations?.sampled_value_count.doc_count ?? 0, }; const valueBuckets: TopValueBucket[] = - aggregations?.sample.sampled_values?.buckets ?? []; + aggregations?.sampled_values?.buckets ?? []; valueBuckets.forEach((bucket) => { stats[`${bucket.key.toString()}Count`] = bucket.doc_count; }); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts index 2775d755c9907..30bebc4c24774 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_stats.test.ts @@ -20,7 +20,6 @@ const params = { includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', - samplerShardSize: 5000, }; export const getExpectedQuery = (aggs: any) => { @@ -46,6 +45,7 @@ export const getExpectedQuery = (aggs: any) => { }, index: 'apm-*', size: 0, + track_total_hits: false, }; }; @@ -55,28 +55,16 @@ describe('field_stats', () => { const req = getNumericFieldStatsRequest(params, 'url.path'); const expectedAggs = { - sample: { - aggs: { - sampled_field_stats: { - aggs: { actual_stats: { stats: { field: 'url.path' } } }, - filter: { exists: { field: 'url.path' } }, - }, - sampled_percentiles: { - percentiles: { - field: 'url.path', - keyed: false, - percents: [50], - }, - }, - sampled_top: { - terms: { - field: 'url.path', - order: { _count: 'desc' }, - size: 10, - }, - }, + sampled_field_stats: { + aggs: { actual_stats: { stats: { field: 'url.path' } } }, + filter: { exists: { field: 'url.path' } }, + }, + sampled_top: { + terms: { + field: 'url.path', + order: { _count: 'desc' }, + size: 10, }, - sampler: { shard_size: 5000 }, }, }; expect(req).toEqual(getExpectedQuery(expectedAggs)); @@ -87,13 +75,8 @@ describe('field_stats', () => { const req = getKeywordFieldStatsRequest(params, 'url.path'); const expectedAggs = { - sample: { - sampler: { shard_size: 5000 }, - aggs: { - sampled_top: { - terms: { field: 'url.path', size: 10, order: { _count: 'desc' } }, - }, - }, + sampled_top: { + terms: { field: 'url.path', size: 10 }, }, }; expect(req).toEqual(getExpectedQuery(expectedAggs)); @@ -104,15 +87,10 @@ describe('field_stats', () => { const req = getBooleanFieldStatsRequest(params, 'url.path'); const expectedAggs = { - sample: { - sampler: { shard_size: 5000 }, - aggs: { - sampled_value_count: { - filter: { exists: { field: 'url.path' } }, - }, - sampled_values: { terms: { field: 'url.path', size: 2 } }, - }, + sampled_value_count: { + filter: { exists: { field: 'url.path' } }, }, + sampled_values: { terms: { field: 'url.path', size: 2 } }, }; expect(req).toEqual(getExpectedQuery(expectedAggs)); }); diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts new file mode 100644 index 0000000000000..0fa508eff508c --- /dev/null +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_field_value_stats.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from 'kibana/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { FieldValuePair } from '../../../../../common/correlations/types'; +import { + FieldStatsCommonRequestParams, + FieldValueFieldStats, + Aggs, + TopValueBucket, +} from '../../../../../common/correlations/field_stats_types'; +import { getQueryWithParams } from '../get_query_with_params'; + +export const getFieldValueFieldStatsRequest = ( + params: FieldStatsCommonRequestParams, + field?: FieldValuePair +): estypes.SearchRequest => { + const query = getQueryWithParams({ params }); + + const { index } = params; + + const size = 0; + const aggs: Aggs = { + filtered_count: { + filter: { + term: { + [`${field?.fieldName}`]: field?.fieldValue, + }, + }, + }, + }; + + const searchBody = { + query, + aggs, + }; + + return { + index, + size, + track_total_hits: false, + body: searchBody, + }; +}; + +export const fetchFieldValueFieldStats = async ( + esClient: ElasticsearchClient, + params: FieldStatsCommonRequestParams, + field: FieldValuePair +): Promise => { + const request = getFieldValueFieldStatsRequest(params, field); + + const { body } = await esClient.search(request); + const aggregations = body.aggregations as { + filtered_count: estypes.AggregationsFiltersBucketItemKeys; + }; + const topValues: TopValueBucket[] = [ + { + key: field.fieldValue, + doc_count: aggregations.filtered_count.doc_count, + }, + ]; + + const stats = { + fieldName: field.fieldName, + topValues, + topValuesSampleSize: aggregations.filtered_count.doc_count ?? 0, + }; + + return stats; +}; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts index 8b41f7662679c..513252ee65e11 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_fields_stats.ts @@ -8,10 +8,7 @@ import { ElasticsearchClient } from 'kibana/server'; import { chunk } from 'lodash'; import { ES_FIELD_TYPES } from '@kbn/field-types'; -import { - FieldValuePair, - CorrelationsParams, -} from '../../../../../common/correlations/types'; +import { FieldValuePair } from '../../../../../common/correlations/types'; import { FieldStats, FieldStatsCommonRequestParams, @@ -23,7 +20,7 @@ import { fetchBooleanFieldStats } from './get_boolean_field_stats'; export const fetchFieldsStats = async ( esClient: ElasticsearchClient, - params: CorrelationsParams, + fieldStatsParams: FieldStatsCommonRequestParams, fieldsToSample: string[], termFilters?: FieldValuePair[] ): Promise<{ stats: FieldStats[]; errors: any[] }> => { @@ -33,14 +30,10 @@ export const fetchFieldsStats = async ( if (fieldsToSample.length === 0) return { stats, errors }; const respMapping = await esClient.fieldCaps({ - ...getRequestBase(params), + ...getRequestBase(fieldStatsParams), fields: fieldsToSample, }); - const fieldStatsParams: FieldStatsCommonRequestParams = { - ...params, - samplerShardSize: 5000, - }; const fieldStatsPromises = Object.entries(respMapping.body.fields) .map(([key, value], idx) => { const field: FieldValuePair = { fieldName: key, fieldValue: '' }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts index c64bbc6678779..16ba4f24f5e93 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_keyword_field_stats.ts @@ -14,7 +14,6 @@ import { Aggs, TopValueBucket, } from '../../../../../common/correlations/field_stats_types'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; import { getQueryWithParams } from '../get_query_with_params'; export const getKeywordFieldStatsRequest = ( @@ -24,7 +23,7 @@ export const getKeywordFieldStatsRequest = ( ): estypes.SearchRequest => { const query = getQueryWithParams({ params, termFilters }); - const { index, samplerShardSize } = params; + const { index } = params; const size = 0; const aggs: Aggs = { @@ -32,23 +31,19 @@ export const getKeywordFieldStatsRequest = ( terms: { field: fieldName, size: 10, - order: { - _count: 'desc', - }, }, }, }; const searchBody = { query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, + aggs, }; return { index, size, + track_total_hits: false, body: searchBody, }; }; @@ -66,19 +61,16 @@ export const fetchKeywordFieldStats = async ( ); const { body } = await esClient.search(request); const aggregations = body.aggregations as { - sample: { - sampled_top: estypes.AggregationsTermsAggregate; - }; + sampled_top: estypes.AggregationsTermsAggregate; }; - const topValues: TopValueBucket[] = - aggregations?.sample.sampled_top?.buckets ?? []; + const topValues: TopValueBucket[] = aggregations?.sampled_top?.buckets ?? []; const stats = { fieldName: field.fieldName, topValues, topValuesSampleSize: topValues.reduce( (acc, curr) => acc + curr.doc_count, - aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + aggregations.sampled_top?.sum_other_doc_count ?? 0 ), }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts index 21e6559fdda25..197ed66c4fe70 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/field_stats/get_numeric_field_stats.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { find, get } from 'lodash'; +import { get } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { NumericFieldStats, @@ -16,10 +16,6 @@ import { } from '../../../../../common/correlations/field_stats_types'; import { FieldValuePair } from '../../../../../common/correlations/types'; import { getQueryWithParams } from '../get_query_with_params'; -import { buildSamplerAggregation } from '../../utils/field_stats_utils'; - -// Only need 50th percentile for the median -const PERCENTILES = [50]; export const getNumericFieldStatsRequest = ( params: FieldStatsCommonRequestParams, @@ -29,9 +25,8 @@ export const getNumericFieldStatsRequest = ( const query = getQueryWithParams({ params, termFilters }); const size = 0; - const { index, samplerShardSize } = params; + const { index } = params; - const percents = PERCENTILES; const aggs: Aggs = { sampled_field_stats: { filter: { exists: { field: fieldName } }, @@ -41,13 +36,6 @@ export const getNumericFieldStatsRequest = ( }, }, }, - sampled_percentiles: { - percentiles: { - field: fieldName, - percents, - keyed: false, - }, - }, sampled_top: { terms: { field: fieldName, @@ -61,14 +49,13 @@ export const getNumericFieldStatsRequest = ( const searchBody = { query, - aggs: { - sample: buildSamplerAggregation(aggs, samplerShardSize), - }, + aggs, }; return { index, size, + track_total_hits: false, body: searchBody, }; }; @@ -87,19 +74,15 @@ export const fetchNumericFieldStats = async ( const { body } = await esClient.search(request); const aggregations = body.aggregations as { - sample: { - sampled_top: estypes.AggregationsTermsAggregate; - sampled_percentiles: estypes.AggregationsHdrPercentilesAggregate; - sampled_field_stats: { - doc_count: number; - actual_stats: estypes.AggregationsStatsAggregate; - }; + sampled_top: estypes.AggregationsTermsAggregate; + sampled_field_stats: { + doc_count: number; + actual_stats: estypes.AggregationsStatsAggregate; }; }; - const docCount = aggregations?.sample.sampled_field_stats?.doc_count ?? 0; - const fieldStatsResp = - aggregations?.sample.sampled_field_stats?.actual_stats ?? {}; - const topValues = aggregations?.sample.sampled_top?.buckets ?? []; + const docCount = aggregations?.sampled_field_stats?.doc_count ?? 0; + const fieldStatsResp = aggregations?.sampled_field_stats?.actual_stats ?? {}; + const topValues = aggregations?.sampled_top?.buckets ?? []; const stats: NumericFieldStats = { fieldName: field.fieldName, @@ -110,20 +93,9 @@ export const fetchNumericFieldStats = async ( topValues, topValuesSampleSize: topValues.reduce( (acc: number, curr: TopValueBucket) => acc + curr.doc_count, - aggregations.sample.sampled_top?.sum_other_doc_count ?? 0 + aggregations.sampled_top?.sum_other_doc_count ?? 0 ), }; - if (stats.count !== undefined && stats.count > 0) { - const percentiles = aggregations?.sample.sampled_percentiles.values ?? []; - const medianPercentile: { value: number; key: number } | undefined = find( - percentiles, - { - key: 50, - } - ); - stats.median = medianPercentile !== undefined ? medianPercentile!.value : 0; - } - return stats; }; diff --git a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts index 548127eb7647d..d2a86a20bd5c6 100644 --- a/x-pack/plugins/apm/server/routes/correlations/queries/index.ts +++ b/x-pack/plugins/apm/server/routes/correlations/queries/index.ts @@ -16,3 +16,4 @@ export { fetchTransactionDurationCorrelation } from './query_correlation'; export { fetchTransactionDurationCorrelationWithHistogram } from './query_correlation_with_histogram'; export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; export { fetchTransactionDurationRanges } from './query_ranges'; +export { fetchFieldValueFieldStats } from './field_stats/get_field_value_stats'; diff --git a/x-pack/plugins/apm/server/routes/correlations/route.ts b/x-pack/plugins/apm/server/routes/correlations/route.ts index b02a6fbc6b7a6..377fedf9d1813 100644 --- a/x-pack/plugins/apm/server/routes/correlations/route.ts +++ b/x-pack/plugins/apm/server/routes/correlations/route.ts @@ -19,6 +19,7 @@ import { fetchSignificantCorrelations, fetchTransactionDurationFieldCandidates, fetchTransactionDurationFieldValuePairs, + fetchFieldValueFieldStats, } from './queries'; import { fetchFieldsStats } from './queries/field_stats/get_fields_stats'; @@ -77,12 +78,12 @@ const fieldStatsRoute = createApmServerRoute({ transactionName: t.string, transactionType: t.string, }), - environmentRt, - kueryRt, - rangeRt, t.type({ fieldsToSample: t.array(t.string), }), + environmentRt, + kueryRt, + rangeRt, ]), }), options: { tags: ['access:apm'] }, @@ -112,6 +113,51 @@ const fieldStatsRoute = createApmServerRoute({ }, }); +const fieldValueStatsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/correlations/field_value_stats', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + fieldName: t.string, + fieldValue: t.union([t.string, t.number]), + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { context } = resources; + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + + const { indices } = await setupRequest(resources); + const esClient = resources.context.core.elasticsearch.client.asCurrentUser; + + const { fieldName, fieldValue, ...params } = resources.params.query; + + return withApmSpan( + 'get_correlations_field_value_stats', + async () => + await fetchFieldValueFieldStats( + esClient, + { + ...params, + index: indices.transaction, + }, + { fieldName, fieldValue } + ) + ); + }, +}); + const fieldValuePairsRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/correlations/field_value_pairs', params: t.type({ @@ -252,5 +298,6 @@ export const correlationsRouteRepository = createApmServerRouteRepository() .add(pValuesRoute) .add(fieldCandidatesRoute) .add(fieldStatsRoute) + .add(fieldValueStatsRoute) .add(fieldValuePairsRoute) .add(significantCorrelationsRoute); diff --git a/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts b/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts index 7f98f771c50e2..a60622583781b 100644 --- a/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts +++ b/x-pack/plugins/apm/server/routes/correlations/utils/field_stats_utils.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - /* * Contains utility functions for building and processing queries. */ @@ -38,22 +36,3 @@ export function buildBaseFilterCriteria( return filterCriteria; } - -// Wraps the supplied aggregations in a sampler aggregation. -// A supplied samplerShardSize (the shard_size parameter of the sampler aggregation) -// of less than 1 indicates no sampling, and the aggs are returned as-is. -export function buildSamplerAggregation( - aggs: any, - samplerShardSize: number -): estypes.AggregationsAggregationContainer { - if (samplerShardSize < 1) { - return aggs; - } - - return { - sampler: { - shard_size: samplerShardSize, - }, - aggs, - }; -} diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts index 7d345b5e3bec1..cdea5cd43f02f 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.test.ts @@ -36,7 +36,7 @@ describe('createStaticDataView', () => { const savedObjectsClient = getMockSavedObjectsClient('apm-*'); await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: false } as APMConfig, + config: { autoCreateApmDataView: false } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -53,7 +53,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -70,7 +70,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -90,7 +90,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); @@ -117,7 +117,7 @@ describe('createStaticDataView', () => { await createStaticDataView({ setup, - config: { autocreateApmIndexPattern: true } as APMConfig, + config: { autoCreateApmDataView: true } as APMConfig, savedObjectsClient, spaceId: 'default', }); diff --git a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts index 20b3d3117dd9f..665f9ca3e96eb 100644 --- a/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts +++ b/x-pack/plugins/apm/server/routes/data_view/create_static_data_view.ts @@ -31,8 +31,8 @@ export async function createStaticDataView({ spaceId?: string; }): Promise { return withApmSpan('create_static_data_view', async () => { - // don't autocreate APM data view if it's been disabled via the config - if (!config.autocreateApmIndexPattern) { + // don't auto-create APM data view if it's been disabled via the config + if (!config.autoCreateApmDataView) { return false; } diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts index 0a0a92760decd..792bc0463aa15 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts @@ -162,17 +162,13 @@ export async function getMLJobs( anomalyDetectors: ReturnType, environment: string ) { - const response = await getMlJobsWithAPMGroup(anomalyDetectors); + const jobs = await getMlJobsWithAPMGroup(anomalyDetectors); // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` // and checking that it is compatable. - const mlJobs = response.jobs.filter( - (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 - ); + const mlJobs = jobs.filter((job) => job.version >= 2); if (environment !== ENVIRONMENT_ALL.value) { - const matchingMLJob = mlJobs.find( - (job) => job.custom_settings?.job_tags?.environment === environment - ); + const matchingMLJob = mlJobs.find((job) => job.environment === environment); if (!matchingMLJob) { return []; } @@ -186,5 +182,5 @@ export async function getMLJobIds( environment: string ) { const mlJobs = await getMLJobs(anomalyDetectors, environment); - return mlJobs.map((job) => job.job_id); + return mlJobs.map((job) => job.jobId); } diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts index a924a9214977d..35089acf38688 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts @@ -11,15 +11,15 @@ import { maxSuggestions } from '../../../../../observability/common'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { ML_ERRORS } from '../../../../common/anomaly_detection'; import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; -import { getAnomalyDetectionJobs } from '../../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../environments/get_all_environments'; -import { hasLegacyJobs } from '../../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../../lib/helpers/transactions'; import { notifyFeatureUsage } from '../../../feature'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { createApmServerRouteRepository } from '../../apm_routes/create_apm_server_route_repository'; +import { updateToV3 } from './update_to_v3'; +import { environmentStringRt } from '../../../../common/environment_rt'; +import { getMlJobsWithAPMGroup } from '../../../lib/anomaly_detection/get_ml_jobs_with_apm_group'; // get ML anomaly detection jobs for each environment const anomalyDetectionJobsRoute = createApmServerRoute({ @@ -29,22 +29,21 @@ const anomalyDetectionJobsRoute = createApmServerRoute({ }, handler: async (resources) => { const setup = await setupRequest(resources); - const { context, logger } = resources; + const { context } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } - const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () => - Promise.all([ - getAnomalyDetectionJobs(setup, logger), - hasLegacyJobs(setup), - ]) - ); + if (!setup.ml) { + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE); + } + + const jobs = await getMlJobsWithAPMGroup(setup.ml?.anomalyDetectors); return { jobs, - hasLegacyJobs: legacyJobs, + hasLegacyJobs: jobs.some((job) => job.version === 1), }; }, }); @@ -57,7 +56,7 @@ const createAnomalyDetectionJobsRoute = createApmServerRoute({ }, params: t.type({ body: t.type({ - environments: t.array(t.string), + environments: t.array(environmentStringRt), }), }), handler: async (resources) => { @@ -107,7 +106,35 @@ const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ }, }); +const anomalyDetectionUpdateToV3Route = createApmServerRoute({ + endpoint: 'POST /internal/apm/settings/anomaly-detection/update_to_v3', + options: { + tags: [ + 'access:apm', + 'access:apm_write', + 'access:ml:canCreateJob', + 'access:ml:canGetJobs', + 'access:ml:canCloseJob', + ], + }, + handler: async (resources) => { + const [setup, esClient] = await Promise.all([ + setupRequest(resources), + resources.core + .start() + .then((start) => start.elasticsearch.client.asInternalUser), + ]); + + const { logger } = resources; + + return { + update: await updateToV3({ setup, logger, esClient }), + }; + }, +}); + export const anomalyDetectionRouteRepository = createApmServerRouteRepository() .add(anomalyDetectionJobsRoute) .add(createAnomalyDetectionJobsRoute) - .add(anomalyDetectionEnvironmentsRoute); + .add(anomalyDetectionEnvironmentsRoute) + .add(anomalyDetectionUpdateToV3Route); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts new file mode 100644 index 0000000000000..b23a28648482e --- /dev/null +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/logging'; +import { uniq } from 'lodash'; +import pLimit from 'p-limit'; +import { ElasticsearchClient } from '../../../../../../../src/core/server'; +import { JOB_STATE } from '../../../../../ml/common'; +import { createAnomalyDetectionJobs } from '../../../lib/anomaly_detection/create_anomaly_detection_jobs'; +import { getAnomalyDetectionJobs } from '../../../lib/anomaly_detection/get_anomaly_detection_jobs'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export async function updateToV3({ + logger, + setup, + esClient, +}: { + logger: Logger; + setup: Setup; + esClient: ElasticsearchClient; +}) { + const allJobs = await getAnomalyDetectionJobs(setup); + + const v2Jobs = allJobs.filter((job) => job.version === 2); + + const activeV2Jobs = v2Jobs.filter( + (job) => + job.jobState === JOB_STATE.OPENED || job.jobState === JOB_STATE.OPENING + ); + + const environments = uniq(v2Jobs.map((job) => job.environment)); + + const limiter = pLimit(3); + + if (!v2Jobs.length) { + return true; + } + + if (activeV2Jobs.length) { + await withApmSpan('anomaly_detection_stop_v2_jobs', () => + Promise.all( + activeV2Jobs.map((job) => + limiter(() => { + return esClient.ml.closeJob({ + job_id: job.jobId, + }); + }) + ) + ) + ); + } + + await createAnomalyDetectionJobs(setup, environments, logger); + + return true; +} diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index a089c7bf3968a..6ec196b2a9b8c 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -37,6 +37,7 @@ export interface APMRouteCreateOptions { | 'access:apm_write' | 'access:ml:canGetJobs' | 'access:ml:canCreateJob' + | 'access:ml:canCloseJob' >; body?: { accepts: Array<'application/json' | 'multipart/form-data'> }; disableTelemetry?: boolean; diff --git a/x-pack/plugins/cases/common/api/cases/alerts.ts b/x-pack/plugins/cases/common/api/cases/alerts.ts index 1a1abb4cbb66a..3647b1acb3a40 100644 --- a/x-pack/plugins/cases/common/api/cases/alerts.ts +++ b/x-pack/plugins/cases/common/api/cases/alerts.ts @@ -14,5 +14,4 @@ const AlertRt = rt.type({ }); export const AlertResponseRt = rt.array(AlertRt); - export type AlertResponse = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index ed0fa1eb0f3ed..8933c70c8eaf0 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -924,6 +924,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases-comments", + }, + }, + "message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "message": "Failed attempt to access a comments as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases-comments", + }, + }, + "message": "User has accessed cases-comments [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "getAttachmentMetrics" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_comment_get_metrics", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "message": "User has accessed a comments as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "getCase" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 057e85b460c2e..3596423860bf3 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -83,11 +83,47 @@ export function isWriteOperation(operation: OperationDetails): boolean { return Object.values(WriteOperations).includes(operation.name as WriteOperations); } -/** - * Definition of all APIs within the cases backend. - */ -export const Operations: Record = { - // case operations +const CaseOperations = { + [ReadOperations.GetCase]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_get', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.ResolveCase]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_resolve', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.FindCases]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_find', + verbs: accessVerbs, + docType: 'cases', + savedObjectType: CASE_SAVED_OBJECT, + }, + [ReadOperations.GetCaseIDsByAlertID]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_ids_by_alert_id_get', + verbs: accessVerbs, + docType: 'cases', + savedObjectType: CASE_COMMENT_SAVED_OBJECT, + }, + [ReadOperations.GetCaseMetrics]: { + ecsType: EVENT_TYPES.access, + name: ACCESS_CASE_OPERATION, + action: 'case_get_metrics', + verbs: accessVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, [WriteOperations.CreateCase]: { ecsType: EVENT_TYPES.creation, name: WriteOperations.CreateCase, @@ -120,6 +156,17 @@ export const Operations: Record = { + ...CaseOperations, + ...ConfigurationOperations, + ...AttachmentOperations, + [ReadOperations.GetTags]: { ecsType: EVENT_TYPES.access, - name: ACCESS_COMMENT_OPERATION, - action: 'case_comment_get_all', + name: ReadOperations.GetTags, + action: 'case_tags_get', verbs: accessVerbs, - docType: 'comments', - savedObjectType: CASE_COMMENT_SAVED_OBJECT, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, }, - [ReadOperations.FindComments]: { + [ReadOperations.GetReporters]: { ecsType: EVENT_TYPES.access, - name: ACCESS_COMMENT_OPERATION, - action: 'case_comment_find', + name: ReadOperations.GetReporters, + action: 'case_reporters_get', verbs: accessVerbs, - docType: 'comments', - savedObjectType: CASE_COMMENT_SAVED_OBJECT, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, }, - // stats operations [ReadOperations.GetCaseStatuses]: { ecsType: EVENT_TYPES.access, name: ACCESS_CASE_OPERATION, @@ -274,7 +291,6 @@ export const Operations: Record => { - const { unsecuredSavedObjectsClient, authorization, attachmentService } = clientArgs; - - // This will perform an authorization check to ensure the user has access to the parent case - const theCase = await casesClient.cases.get({ - id: caseId, - includeComments: false, - includeSubCaseComments: false, - }); - - const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = - await authorization.getAuthorizationFilter(Operations.getAlertsAttachedToCase); - - const alerts = await attachmentService.getAllAlertsAttachToCase({ - unsecuredSavedObjectsClient, - caseId: theCase.id, - filter: authorizationFilter, - }); - - ensureSavedObjectsAreAuthorized( - alerts.map((alert) => ({ - owner: alert.attributes.owner, - id: alert.id, - })) - ); - - return normalizeAlertResponse(alerts); + const { unsecuredSavedObjectsClient, authorization, attachmentService, logger } = clientArgs; + + try { + // This will perform an authorization check to ensure the user has access to the parent case + const theCase = await casesClient.cases.get({ + id: caseId, + includeComments: false, + includeSubCaseComments: false, + }); + + const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = + await authorization.getAuthorizationFilter(Operations.getAlertsAttachedToCase); + + const alerts = await attachmentService.getAllAlertsAttachToCase({ + unsecuredSavedObjectsClient, + caseId: theCase.id, + filter: authorizationFilter, + }); + + ensureSavedObjectsAreAuthorized( + alerts.map((alert) => ({ + owner: alert.attributes.owner, + id: alert.id, + })) + ); + + return normalizeAlertResponse(alerts); + } catch (error) { + throw createCaseError({ + message: `Failed to get alerts attached to case id: ${caseId}: ${error}`, + error, + logger, + }); + } }; /** diff --git a/x-pack/plugins/cases/server/client/metrics/alerts_count.ts b/x-pack/plugins/cases/server/client/metrics/alerts_count.ts index aa0e945bc5fcf..118761acb3680 100644 --- a/x-pack/plugins/cases/server/client/metrics/alerts_count.ts +++ b/x-pack/plugins/cases/server/client/metrics/alerts_count.ts @@ -6,18 +6,56 @@ */ import { CaseMetricsResponse } from '../../../common/api'; +import { Operations } from '../../authorization'; +import { createCaseError } from '../../common/error'; +import { CasesClient } from '../client'; +import { CasesClientArgs } from '../types'; import { MetricsHandler } from './types'; export class AlertsCount implements MetricsHandler { + constructor( + private readonly caseId: string, + private readonly casesClient: CasesClient, + private readonly clientArgs: CasesClientArgs + ) {} + public getFeatures(): Set { return new Set(['alertsCount']); } public async compute(): Promise { - return { - alerts: { - count: 0, - }, - }; + const { unsecuredSavedObjectsClient, authorization, attachmentService, logger } = + this.clientArgs; + + try { + // This will perform an authorization check to ensure the user has access to the parent case + const theCase = await this.casesClient.cases.get({ + id: this.caseId, + includeComments: false, + includeSubCaseComments: false, + }); + + const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( + Operations.getAttachmentMetrics + ); + + const alertsCount = await attachmentService.countAlertsAttachedToCase({ + unsecuredSavedObjectsClient, + caseId: theCase.id, + filter: authorizationFilter, + }); + + return { + alerts: { + count: alertsCount ?? 0, + }, + }; + } catch (error) { + throw createCaseError({ + message: `Failed to count alerts attached case id: ${this.caseId}: ${error}`, + error, + logger, + }); + } } } diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts index cd3c9204e3c03..b192e681df109 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts @@ -11,7 +11,7 @@ import { createCasesClientMock } from '../mocks'; import { CasesClientArgs } from '../types'; import { createAuthorizationMock } from '../../authorization/mock'; import { loggingSystemMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { createCaseServiceMock } from '../../services/mocks'; +import { createAttachmentServiceMock, createCaseServiceMock } from '../../services/mocks'; import { SavedObject } from 'kibana/server'; describe('getMetrics', () => { @@ -28,7 +28,16 @@ describe('getMetrics', () => { } as unknown as CaseResponse; }); + const attachmentService = createAttachmentServiceMock(); + attachmentService.countAlertsAttachedToCase.mockImplementation(async () => { + return 5; + }); + const authorization = createAuthorizationMock(); + authorization.getAuthorizationFilter.mockImplementation(async () => { + return { filter: undefined, ensureSavedObjectsAreAuthorized: () => {} }; + }); + const soClient = savedObjectsClientMock.create(); const caseService = createCaseServiceMock(); caseService.getCase.mockImplementation(async () => { @@ -47,6 +56,7 @@ describe('getMetrics', () => { unsecuredSavedObjectsClient: soClient, caseService, logger, + attachmentService, } as unknown as CasesClientArgs; beforeEach(() => { @@ -100,7 +110,7 @@ describe('getMetrics', () => { clientArgs ); - expect(metrics.alerts?.count).toBeDefined(); + expect(metrics.alerts?.count).toEqual(5); expect(metrics.alerts?.hosts).toBeDefined(); }); diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts index 74628ebd8c9ee..0cc089a1c0882 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts @@ -5,6 +5,7 @@ * 2.0. */ import { merge } from 'lodash'; +import Boom from '@hapi/boom'; import { CaseMetricsResponseRt, CaseMetricsResponse } from '../../../common/api'; import { Operations } from '../../authorization'; @@ -33,23 +34,33 @@ export const getCaseMetrics = async ( casesClient: CasesClient, clientArgs: CasesClientArgs ): Promise => { - const handlers = buildHandlers(params, casesClient, clientArgs); - await checkAuthorization(params, clientArgs); - checkAndThrowIfInvalidFeatures(params, handlers, clientArgs); + const { logger } = clientArgs; - const computedMetrics = await Promise.all( - params.features.map(async (feature) => { - const handler = handlers.get(feature); + try { + const handlers = buildHandlers(params, casesClient, clientArgs); + await checkAuthorization(params, clientArgs); + checkAndThrowIfInvalidFeatures(params, handlers); - return handler?.compute(); - }) - ); + const computedMetrics = await Promise.all( + params.features.map(async (feature) => { + const handler = handlers.get(feature); - const mergedResults = computedMetrics.reduce((acc, metric) => { - return merge(acc, metric); - }, {}); + return handler?.compute(); + }) + ); - return CaseMetricsResponseRt.encode(mergedResults ?? {}); + const mergedResults = computedMetrics.reduce((acc, metric) => { + return merge(acc, metric); + }, {}); + + return CaseMetricsResponseRt.encode(mergedResults ?? {}); + } catch (error) { + throw createCaseError({ + logger, + message: `Failed to retrieve metrics within client for case id: ${params.caseId}: ${error}`, + error, + }); + } }; const buildHandlers = ( @@ -59,7 +70,7 @@ const buildHandlers = ( ): Map => { const handlers = [ new Lifespan(params.caseId, casesClient), - new AlertsCount(), + new AlertsCount(params.caseId, casesClient, clientArgs), new AlertDetails(), new Connectors(), ]; @@ -75,18 +86,16 @@ const buildHandlers = ( const checkAndThrowIfInvalidFeatures = ( params: CaseMetricsParams, - handlers: Map, - clientArgs: CasesClientArgs + handlers: Map ) => { const invalidFeatures = params.features.filter((feature) => !handlers.has(feature)); if (invalidFeatures.length > 0) { const invalidFeaturesAsString = invalidFeatures.join(', '); const validFeaturesAsString = [...handlers.keys()].join(', '); - throw createCaseError({ - logger: clientArgs.logger, - message: `invalid features: [${invalidFeaturesAsString}], please only provide valid features: [${validFeaturesAsString}]`, - }); + throw Boom.badRequest( + `invalid features: [${invalidFeaturesAsString}], please only provide valid features: [${validFeaturesAsString}]` + ); } }; diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index f4e858eb0ed4f..9553f7c7ed1e2 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -12,6 +12,7 @@ import { SavedObjectsUpdateOptions, } from 'kibana/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { KueryNode } from '@kbn/es-query'; import { AttributesTypeAlerts, @@ -26,12 +27,15 @@ import { } from '../../../common/constants'; import { ClientArgs } from '..'; import { buildFilter, combineFilters } from '../../client/utils'; +import { defaultSortField } from '../../common/utils'; interface GetAllAlertsAttachToCaseArgs extends ClientArgs { caseId: string; filter?: KueryNode; } +type CountAlertsAttachedToCaseArgs = GetAllAlertsAttachToCaseArgs; + interface GetAttachmentArgs extends ClientArgs { attachmentId: string; } @@ -57,6 +61,51 @@ interface BulkUpdateAttachmentArgs extends ClientArgs { export class AttachmentService { constructor(private readonly log: Logger) {} + public async countAlertsAttachedToCase({ + unsecuredSavedObjectsClient, + caseId, + filter, + }: CountAlertsAttachedToCaseArgs): Promise { + try { + this.log.debug(`Attempting to count alerts for case id ${caseId}`); + const alertsFilter = buildFilter({ + filters: [CommentType.alert, CommentType.generatedAlert], + field: 'type', + operator: 'or', + type: CASE_COMMENT_SAVED_OBJECT, + }); + + const combinedFilter = combineFilters([alertsFilter, filter]); + + const response = await unsecuredSavedObjectsClient.find< + AttachmentAttributes, + { alerts: { value: number } } + >({ + type: CASE_COMMENT_SAVED_OBJECT, + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: this.buildCountAlertsAggs(), + filter: combinedFilter, + }); + + return response.aggregations?.alerts?.value; + } catch (error) { + this.log.error(`Error while counting alerts for case id ${caseId}: ${error}`); + throw error; + } + } + + private buildCountAlertsAggs(): Record { + return { + alerts: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, + }; + } + /** * Retrieves all the alerts attached to a case. */ diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index f46bcd0906c60..3e68126967512 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -111,6 +111,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => { update: jest.fn(), bulkUpdate: jest.fn(), getAllAlertsAttachToCase: jest.fn(), + countAlertsAttachedToCase: jest.fn(), }; // the cast here is required because jest.Mocked tries to include private members and would throw an error diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx index 1810b05a938da..9f46d6750590f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import '../../../../../__mocks__/shallow_useeffect.mock'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -15,21 +16,26 @@ import { EuiCodeBlock, EuiFlyout, EuiTab, EuiTabs } from '@elastic/eui'; import { Loading } from '../../../../../shared/loading'; -import { CrawlDetailActions, CrawlDetailValues } from '../../crawl_detail_logic'; import { CrawlRequestWithDetailsFromServer } from '../../types'; import { CrawlDetailsPreview } from './crawl_details_preview'; import { CrawlDetailsFlyout } from '.'; -const MOCK_VALUES: Partial = { +const MOCK_VALUES = { dataLoading: false, flyoutClosed: false, crawlRequestFromServer: {} as CrawlRequestWithDetailsFromServer, + logRetention: { + crawler: { + enabled: true, + }, + }, }; -const MOCK_ACTIONS: Partial = { +const MOCK_ACTIONS = { setSelectedTab: jest.fn(), + fetchLogRetention: jest.fn(), }; describe('CrawlDetailsFlyout', () => { @@ -38,6 +44,7 @@ describe('CrawlDetailsFlyout', () => { }); it('renders a flyout ', () => { + setMockActions(MOCK_ACTIONS); setMockValues(MOCK_VALUES); const wrapper = shallow(); @@ -82,7 +89,22 @@ describe('CrawlDetailsFlyout', () => { it('shows the human readable version of the crawl details', () => { const wrapper = shallow(); - expect(wrapper.find(CrawlDetailsPreview)).toHaveLength(1); + const crawlDetailsPreview = wrapper.find(CrawlDetailsPreview); + expect(crawlDetailsPreview).toHaveLength(1); + expect(crawlDetailsPreview.prop('crawlerLogsEnabled')).toEqual(true); + }); + + it('shows the preview differently if the crawler logs are disabled', () => { + setMockValues({ + ...MOCK_VALUES, + selectedTab: 'preview', + logRetention: null, + }); + const wrapper = shallow(); + + const crawlDetailsPreview = wrapper.find(CrawlDetailsPreview); + expect(crawlDetailsPreview).toHaveLength(1); + expect(crawlDetailsPreview.prop('crawlerLogsEnabled')).toEqual(false); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx index 9c3c1da534f72..f1bfd3e5e5e9b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_flyout.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; @@ -21,6 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../../shared/loading'; +import { LogRetentionLogic } from '../../../log_retention'; import { CrawlDetailLogic } from '../../crawl_detail_logic'; import { CrawlDetailsPreview } from './crawl_details_preview'; @@ -29,6 +30,12 @@ export const CrawlDetailsFlyout: React.FC = () => { const { closeFlyout, setSelectedTab } = useActions(CrawlDetailLogic); const { crawlRequestFromServer, dataLoading, flyoutClosed, selectedTab } = useValues(CrawlDetailLogic); + const { fetchLogRetention } = useActions(LogRetentionLogic); + const { logRetention } = useValues(LogRetentionLogic); + + useEffect(() => { + fetchLogRetention(); + }, []); if (flyoutClosed) { return null; @@ -73,7 +80,11 @@ export const CrawlDetailsFlyout: React.FC = () => { ) : ( <> - {selectedTab === 'preview' && } + {selectedTab === 'preview' && ( + + )} {selectedTab === 'json' && ( {JSON.stringify(crawlRequestFromServer, null, 2)} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx index 646c611901c7f..f97e2ff913150 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.test.tsx @@ -9,12 +9,14 @@ import { setMockValues } from '../../../../../__mocks__/kea_logic'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { set } from 'lodash/fp'; import { CrawlDetailValues } from '../../crawl_detail_logic'; import { CrawlerStatus, CrawlType } from '../../types'; import { AccordionList } from './accordion_list'; import { CrawlDetailsPreview } from './crawl_details_preview'; +import { CrawlDetailsSummary } from './crawl_details_summary'; const MOCK_VALUES: Partial = { crawlRequest: { @@ -28,6 +30,15 @@ const MOCK_VALUES: Partial = { domainAllowlist: ['https://www.elastic.co', 'https://www.swiftype.com'], seedUrls: ['https://www.elastic.co/docs', 'https://www.swiftype.com/documentation'], sitemapUrls: ['https://www.elastic.co/sitemap.xml', 'https://www.swiftype.com/sitemap.xml'], + maxCrawlDepth: 10, + }, + stats: { + status: { + urlsAllowed: 10, + pagesVisited: 10, + crawlDurationMSec: 36000, + avgResponseTimeMSec: 100, + }, }, }, }; @@ -38,16 +49,44 @@ describe('CrawlDetailsPreview', () => { crawlRequest: null, }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(true); }); describe('when a crawl request has been loaded', () => { let wrapper: ShallowWrapper; - beforeAll(() => { + beforeEach(() => { setMockValues(MOCK_VALUES); - wrapper = shallow(); + wrapper = shallow(); + }); + + it('contains a summary', () => { + const summary = wrapper.find(CrawlDetailsSummary); + expect(summary.props()).toEqual({ + crawlDepth: 10, + crawlType: 'full', + crawlerLogsEnabled: true, + domainCount: 2, + stats: { + status: { + avgResponseTimeMSec: 100, + crawlDurationMSec: 36000, + pagesVisited: 10, + urlsAllowed: 10, + }, + }, + }); + }); + + it('will default values on summary if missing', () => { + const values = set('crawlRequest.stats', undefined, MOCK_VALUES); + setMockValues(values); + wrapper = shallow(); + + const summary = wrapper.find(CrawlDetailsSummary); + expect(summary.prop('crawlerLogsEnabled')).toEqual(false); + expect(summary.prop('stats')).toEqual(null); }); it('contains a list of domains', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx index 6f837d1db26e2..a9f3d95edf1fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_preview.tsx @@ -15,8 +15,15 @@ import { i18n } from '@kbn/i18n'; import { CrawlDetailLogic } from '../../crawl_detail_logic'; import { AccordionList } from './accordion_list'; +import { CrawlDetailsSummary } from './crawl_details_summary'; -export const CrawlDetailsPreview: React.FC = () => { +interface CrawlDetailsPreviewProps { + crawlerLogsEnabled?: boolean; +} + +export const CrawlDetailsPreview: React.FC = ({ + crawlerLogsEnabled = false, +}) => { const { crawlRequest } = useValues(CrawlDetailLogic); if (crawlRequest === null) { @@ -25,6 +32,14 @@ export const CrawlDetailsPreview: React.FC = () => { return ( <> + + 0} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.test.tsx new file mode 100644 index 0000000000000..f37060a9cef42 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiPanel } from '@elastic/eui'; + +import { CrawlDetailsSummary } from './crawl_details_summary'; + +const MOCK_PROPS = { + crawlDepth: 8, + crawlerLogsEnabled: true, + crawlType: 'full', + domainCount: 15, + stats: { + status: { + urlsAllowed: 108, + crawlDurationMSec: 748382, + pagesVisited: 108, + avgResponseTimeMSec: 42, + statusCodes: { + 401: 4, + 404: 8, + 500: 0, + 503: 3, + }, + }, + }, +}; + +describe('CrawlDetailsSummary', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders as a panel with all fields', () => { + expect(wrapper.is(EuiPanel)).toBe(true); + }); + + it('renders the proper count for errors', () => { + const serverErrors = wrapper.find({ 'data-test-subj': 'serverErrors' }); + const clientErrors = wrapper.find({ 'data-test-subj': 'clientErrors' }); + + expect(serverErrors.prop('title')).toEqual(3); + expect(clientErrors.prop('title')).toEqual(12); + }); + + it('handles missing stats gracefully', () => { + wrapper.setProps({ stats: {} }); + expect(wrapper.find({ 'data-test-subj': 'crawlDuration' }).prop('title')).toEqual('--'); + expect(wrapper.find({ 'data-test-subj': 'pagesVisited' }).prop('title')).toEqual('--'); + expect(wrapper.find({ 'data-test-subj': 'avgResponseTime' }).prop('title')).toEqual('--'); + }); + + it('renders the stat object when logs are disabled but stats are not null', () => { + wrapper.setProps({ crawlerLogsEnabled: false }); + expect(wrapper.find({ 'data-test-subj': 'crawlDuration' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'pagesVisited' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'avgResponseTime' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'urlsAllowed' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-subj': 'logsDisabledMessage' })).toHaveLength(0); + }); + + it('renders a message to enable logs when crawler logs are disabled and stats are null', () => { + wrapper.setProps({ crawlerLogsEnabled: false, stats: null }); + expect(wrapper.find({ 'data-test-subj': 'crawlDuration' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'pagesVisited' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'avgResponseTime' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'urlsAllowed' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-subj': 'logsDisabledMessage' })).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.tsx new file mode 100644 index 0000000000000..e05cbd9101de6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_details_flyout/crawl_details_summary.tsx @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import moment from 'moment'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiStat, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CrawlRequestStats } from '../../types'; + +interface ICrawlerSummaryProps { + crawlDepth: number; + crawlType: string; + crawlerLogsEnabled: boolean; + domainCount: number; + stats: CrawlRequestStats | null; +} + +export const CrawlDetailsSummary: React.FC = ({ + crawlDepth, + crawlType, + crawlerLogsEnabled, + domainCount, + stats, +}) => { + const duration = () => { + if (stats && stats.status && stats.status.crawlDurationMSec) { + const milliseconds = moment.duration(stats.status.crawlDurationMSec, 'milliseconds'); + const hours = milliseconds.hours(); + const minutes = milliseconds.minutes(); + const seconds = milliseconds.seconds(); + return `${hours}h ${minutes}m ${seconds}s`; + } else { + return '--'; + } + }; + + const getStatusCount = (code: string, codes: { [code: string]: number }) => { + return Object.entries(codes).reduce((count, [k, v]) => { + if (k[0] !== code) return count; + return v + count; + }, 0); + }; + + const statusCounts = { + clientErrorCount: + stats && stats.status && stats.status.statusCodes + ? getStatusCount('4', stats.status.statusCodes) + : 0, + serverErrorCount: + stats && stats.status && stats.status.statusCodes + ? getStatusCount('5', stats.status.statusCodes) + : 0, + }; + + const shouldHideStats = !crawlerLogsEnabled && !stats; + + return ( + + + + + + + + + {!shouldHideStats && ( + + + + )} + + + {!shouldHideStats ? ( + + + + URLs{' '} + + + } + /> + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlDetailsSummary.pagesVisitedTooltipTitle', + { + defaultMessage: 'Pages', + } + )}{' '} + + + } + /> + + + + + + + + + + + + ) : ( + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.crawlDetailsSummary.logsDisabledMessage', + { + defaultMessage: + 'Enable Web Crawler logs in settings for more detailed crawl statistics.', + } + )} +

+
+ )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx index fd1f03c586f12..bc5efc7714495 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_event_type_badge.test.tsx @@ -31,6 +31,7 @@ const MOCK_EVENT: CrawlEvent = { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx index 03e5d835df9b3..c2b36f24d7582 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawl_requests_table.test.tsx @@ -35,6 +35,7 @@ const values: { events: CrawlEvent[] } = { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, { @@ -49,6 +50,7 @@ const values: { events: CrawlEvent[] } = { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts index a7d795c93e0a7..152fe0f64de4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawl_detail_logic.test.ts @@ -35,6 +35,19 @@ const crawlRequestResponse: CrawlRequestWithDetailsFromServer = { domain_allowlist: [], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, + }, + stats: { + status: { + urls_allowed: 4, + pages_visited: 4, + crawl_duration_msec: 100, + avg_response_time_msec: 10, + status_codes: { + 200: 4, + 404: 0, + }, + }, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index 5af9b1652c889..0735b5262a20a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -138,6 +138,7 @@ describe('CrawlerLogic', () => { domainAllowlist: ['elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index 0d2c2e60abfa9..440e29e6d3002 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -80,6 +80,7 @@ const events: CrawlEventFromServer[] = [ domain_allowlist: ['moviedatabase.com', 'swiftype.com'], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, }, { @@ -94,6 +95,7 @@ const events: CrawlEventFromServer[] = [ domain_allowlist: ['swiftype.com'], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 85ebb0032971d..3d8881601ae1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -199,28 +199,54 @@ export interface CrawlRequest { completedAt: string | null; } +export interface CrawlRequestStats { + status: { + avgResponseTimeMSec?: number; + crawlDurationMSec?: number; + pagesVisited?: number; + urlsAllowed?: number; + statusCodes?: { + [code: string]: number; + }; + }; +} + +export interface CrawlRequestStatsFromServer { + status: { + avg_response_time_msec?: number; + crawl_duration_msec?: number; + pages_visited?: number; + urls_allowed?: number; + status_codes?: { + [code: string]: number; + }; + }; +} + export interface CrawlConfig { domainAllowlist: string[]; seedUrls: string[]; sitemapUrls: string[]; + maxCrawlDepth: number; } export interface CrawlConfigFromServer { domain_allowlist: string[]; seed_urls: string[]; sitemap_urls: string[]; + max_crawl_depth: number; } export type CrawlRequestWithDetailsFromServer = CrawlRequestFromServer & { type: CrawlType; crawl_config: CrawlConfigFromServer; - // TODO add other properties like stats + stats: CrawlRequestStatsFromServer; }; export type CrawlRequestWithDetails = CrawlRequest & { type: CrawlType; crawlConfig: CrawlConfig; - // TODO add other properties like stats + stats: CrawlRequestStats | null; }; export type CrawlEventStage = 'crawl' | 'process'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts index 0df1f57eaefa0..cab4023370291 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.test.ts @@ -22,6 +22,7 @@ import { CrawlRequestWithDetails, CrawlEvent, CrawlEventFromServer, + CrawlRequestStatsFromServer, } from './types'; import { @@ -34,6 +35,7 @@ import { getDeleteDomainConfirmationMessage, getDeleteDomainSuccessMessage, getCrawlRulePathPatternTooltip, + crawlRequestStatsServerToClient, } from './utils'; const DEFAULT_CRAWL_RULE: CrawlRule = { @@ -126,6 +128,36 @@ describe('crawlRequestServerToClient', () => { }); }); +describe('crawlRequestStatsServerToClient', () => { + it('converts the API payload into properties matching our code style', () => { + const defaultServerPayload: CrawlRequestStatsFromServer = { + status: { + urls_allowed: 4, + pages_visited: 4, + crawl_duration_msec: 100, + avg_response_time_msec: 10, + status_codes: { + 200: 4, + 404: 0, + }, + }, + }; + + expect(crawlRequestStatsServerToClient(defaultServerPayload)).toEqual({ + status: { + urlsAllowed: 4, + pagesVisited: 4, + crawlDurationMSec: 100, + avgResponseTimeMSec: 10, + statusCodes: { + 200: 4, + 404: 0, + }, + }, + }); + }); +}); + describe('crawlRequestWithDetailsServerToClient', () => { it('converts the API payload into properties matching our code style', () => { const id = '507f1f77bcf86cd799439011'; @@ -141,6 +173,19 @@ describe('crawlRequestWithDetailsServerToClient', () => { domain_allowlist: [], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, + }, + stats: { + status: { + urls_allowed: 4, + pages_visited: 4, + crawl_duration_msec: 100, + avg_response_time_msec: 10, + status_codes: { + 200: 4, + 404: 0, + }, + }, }, }; @@ -155,6 +200,19 @@ describe('crawlRequestWithDetailsServerToClient', () => { domainAllowlist: [], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, + }, + stats: { + status: { + urlsAllowed: 4, + pagesVisited: 4, + crawlDurationMSec: 100, + avgResponseTimeMSec: 10, + statusCodes: { + 200: 4, + 404: 0, + }, + }, }, }; @@ -191,6 +249,7 @@ describe('crawlEventServerToClient', () => { domain_allowlist: [], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, stage: 'crawl', }; @@ -206,6 +265,7 @@ describe('crawlEventServerToClient', () => { domainAllowlist: [], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, stage: 'crawl', }; @@ -274,6 +334,7 @@ describe('crawlerDataServerToClient', () => { domain_allowlist: ['https://www.elastic.co'], seed_urls: [], sitemap_urls: [], + max_crawl_depth: 10, }, }, ], @@ -329,6 +390,7 @@ describe('crawlerDataServerToClient', () => { domainAllowlist: ['https://www.elastic.co'], seedUrls: [], sitemapUrls: [], + maxCrawlDepth: 10, }, }, ]); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts index d1203e19c0208..4819b073cccb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/utils.ts @@ -16,6 +16,8 @@ import { CrawlerDomainValidationStep, CrawlRequestFromServer, CrawlRequest, + CrawlRequestStats, + CrawlRequestStatsFromServer, CrawlRule, CrawlerRules, CrawlEventFromServer, @@ -66,6 +68,30 @@ export function crawlerDomainServerToClient(payload: CrawlerDomainFromServer): C return clientPayload; } +export function crawlRequestStatsServerToClient( + crawlStats: CrawlRequestStatsFromServer +): CrawlRequestStats { + const { + status: { + avg_response_time_msec: avgResponseTimeMSec, + crawl_duration_msec: crawlDurationMSec, + pages_visited: pagesVisited, + urls_allowed: urlsAllowed, + status_codes: statusCodes, + }, + } = crawlStats; + + return { + status: { + urlsAllowed, + pagesVisited, + avgResponseTimeMSec, + crawlDurationMSec, + statusCodes, + }, + }; +} + export function crawlRequestServerToClient(crawlRequest: CrawlRequestFromServer): CrawlRequest { const { id, @@ -89,12 +115,14 @@ export function crawlConfigServerToClient(crawlConfig: CrawlConfigFromServer): C domain_allowlist: domainAllowlist, seed_urls: seedUrls, sitemap_urls: sitemapUrls, + max_crawl_depth: maxCrawlDepth, } = crawlConfig; return { domainAllowlist, seedUrls, sitemapUrls, + maxCrawlDepth, }; } @@ -126,24 +154,25 @@ export function crawlRequestWithDetailsServerToClient( event: CrawlRequestWithDetailsFromServer ): CrawlRequestWithDetails { const { - id, - status, - created_at: createdAt, began_at: beganAt, completed_at: completedAt, - type, crawl_config: crawlConfig, + created_at: createdAt, + id, + stats: crawlStats, + status, + type, } = event; return { - id, - status, - createdAt, beganAt, completedAt, - type, crawlConfig: crawlConfigServerToClient(crawlConfig), - // TODO add fields like stats + createdAt, + id, + stats: crawlStats && crawlRequestStatsServerToClient(crawlStats), + status, + type, }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx index 270daf195bd38..7bf80b5ff9180 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.tsx @@ -9,8 +9,9 @@ import React from 'react'; import { EuiLink, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; + import { EXPLORE_PLATINUM_FEATURES_LINK } from '../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; interface LicenseCalloutProps { message?: string; @@ -20,7 +21,7 @@ export const LicenseCallout: React.FC = ({ message }) => { const title = ( <> {message}{' '} - + {EXPLORE_PLATINUM_FEATURES_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 1b630a47e2f86..ee180ae52e0b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -7,8 +7,6 @@ import { generatePath } from 'react-router-dom'; -import { docLinks } from '../shared/doc_links'; - import { GITHUB_VIA_APP_SERVICE_TYPE, GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, @@ -22,35 +20,6 @@ export const LOGOUT_ROUTE = '/logout'; export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; -export const BOX_DOCS_URL = docLinks.workplaceSearchBox; -export const CONFLUENCE_DOCS_URL = docLinks.workplaceSearchConfluenceCloud; -export const CONFLUENCE_SERVER_DOCS_URL = docLinks.workplaceSearchConfluenceServer; -export const CUSTOM_SOURCE_DOCS_URL = docLinks.workplaceSearchCustomSources; -export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = - docLinks.workplaceSearchCustomSourcePermissions; -export const DIFFERENT_SYNC_TYPES_DOCS_URL = docLinks.workplaceSearchIndexingSchedule; -export const DOCUMENT_PERMISSIONS_DOCS_URL = docLinks.workplaceSearchDocumentPermissions; -export const DROPBOX_DOCS_URL = docLinks.workplaceSearchDropbox; -export const ENT_SEARCH_LICENSE_MANAGEMENT = docLinks.licenseManagement; -export const EXTERNAL_IDENTITIES_DOCS_URL = docLinks.workplaceSearchExternalIdentities; -export const GETTING_STARTED_DOCS_URL = docLinks.workplaceSearchGettingStarted; -export const GITHUB_DOCS_URL = docLinks.workplaceSearchGitHub; -export const GITHUB_ENTERPRISE_DOCS_URL = docLinks.workplaceSearchGitHub; -export const GMAIL_DOCS_URL = docLinks.workplaceSearchGmail; -export const GOOGLE_DRIVE_DOCS_URL = docLinks.workplaceSearchGoogleDrive; -export const JIRA_DOCS_URL = docLinks.workplaceSearchJiraCloud; -export const JIRA_SERVER_DOCS_URL = docLinks.workplaceSearchJiraServer; -export const OBJECTS_AND_ASSETS_DOCS_URL = docLinks.workplaceSearchSynch; -export const ONEDRIVE_DOCS_URL = docLinks.workplaceSearchOneDrive; -export const PRIVATE_SOURCES_DOCS_URL = docLinks.workplaceSearchPermissions; -export const SALESFORCE_DOCS_URL = docLinks.workplaceSearchSalesforce; -export const SECURITY_DOCS_URL = docLinks.workplaceSearchSecurity; -export const SERVICENOW_DOCS_URL = docLinks.workplaceSearchServiceNow; -export const SHAREPOINT_DOCS_URL = docLinks.workplaceSearchSharePoint; -export const SLACK_DOCS_URL = docLinks.workplaceSearchSlack; -export const SYNCHRONIZATION_DOCS_URL = docLinks.workplaceSearchSynch; -export const ZENDESK_DOCS_URL = docLinks.workplaceSearchZendesk; - export const PERSONAL_PATH = '/p'; export const OAUTH_AUTHORIZE_PATH = `${PERSONAL_PATH}/oauth/authorize`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index 167bf1af4b9b1..9b34053bfe524 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -21,13 +21,9 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../../shared/doc_links'; import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers'; -import { - getSourcesPath, - ADD_SOURCE_PATH, - SECURITY_PATH, - PRIVATE_SOURCES_DOCS_URL, -} from '../../../../routes'; +import { getSourcesPath, ADD_SOURCE_PATH, SECURITY_PATH } from '../../../../routes'; import { CONFIG_COMPLETED_PRIVATE_SOURCES_DISABLED_LINK, @@ -126,7 +122,7 @@ export const ConfigCompleted: React.FC = ({ {CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx index 4682d4329a964..e794323dc169e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { CUSTOM_SOURCE_DOCS_URL } from '../../../../routes'; +import { docLinks } from '../../../../../shared/doc_links'; import { SOURCE_NAME_LABEL } from '../../constants'; @@ -63,7 +63,7 @@ export const ConfigureCustom: React.FC = ({ defaultMessage="{link} to learn more about Custom API Sources." values={{ link: ( - + {CONFIG_CUSTOM_LINK_TEXT} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx index 3c6980f74bcf5..d3879eabe08de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_callout.tsx @@ -17,8 +17,8 @@ import { EuiText, } from '@elastic/eui'; +import { docLinks } from '../../../../../shared/doc_links'; import { EXPLORE_PLATINUM_FEATURES_LINK } from '../../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; import { SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE, @@ -45,7 +45,7 @@ export const DocumentPermissionsCallout: React.FC = () => { - + {EXPLORE_PLATINUM_FEATURES_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx index 1b1043ecbc3d2..1cc953ee7c2ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/document_permissions_field.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; +import { docLinks } from '../../../../../shared/doc_links'; import { LEARN_MORE_LINK } from '../../constants'; import { @@ -42,7 +42,7 @@ export const DocumentPermissionsField: React.FC = ({ setValue, }) => { const whichDocsLink = ( - + {CONNECT_WHICH_OPTION_LINK} ); @@ -64,7 +64,7 @@ export const DocumentPermissionsField: React.FC = ({ defaultMessage="Document-level permissions are not yet available for this source. {link}" values={{ link: ( - + {LEARN_MORE_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index bbf1b66277c70..9dbbcc537fa31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -24,14 +24,13 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../../shared/doc_links'; import { LicensingLogic } from '../../../../../shared/licensing'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { LicenseBadge } from '../../../../components/shared/license_badge'; import { SOURCES_PATH, SOURCE_DISPLAY_SETTINGS_PATH, - CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL, - ENT_SEARCH_LICENSE_MANAGEMENT, getContentSourcePath, getSourcesPath, } from '../../../../routes'; @@ -178,7 +177,10 @@ export const SaveCustom: React.FC = ({ defaultMessage="{link} manage content access content on individual or group attributes. Allow or deny access to specific documents." values={{ link: ( - + {SAVE_CUSTOM_DOC_PERMISSIONS_LINK} ), @@ -189,7 +191,7 @@ export const SaveCustom: React.FC = ({ {!hasPlatinumLicense && ( - + {LEARN_CUSTOM_FEATURES_BUTTON} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index 29abbf94db397..d3714c2174b66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -33,6 +33,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { CANCEL_BUTTON_LABEL, START_BUTTON_LABEL } from '../../../../shared/constants'; +import { docLinks } from '../../../../shared/doc_links'; import { EuiListGroupItemTo, EuiLinkTo } from '../../../../shared/react_router_helpers'; import { AppLogic } from '../../../app_logic'; import aclImage from '../../../assets/supports_acl.svg'; @@ -46,10 +47,6 @@ import { DOCUMENTATION_LINK_TITLE, } from '../../../constants'; import { - CUSTOM_SOURCE_DOCS_URL, - DOCUMENT_PERMISSIONS_DOCS_URL, - ENT_SEARCH_LICENSE_MANAGEMENT, - EXTERNAL_IDENTITIES_DOCS_URL, SYNC_FREQUENCY_PATH, BLOCKED_TIME_WINDOWS_PATH, getGroupPath, @@ -347,7 +344,7 @@ export const Overview: React.FC = () => { defaultMessage="{learnMoreLink} about permissions" values={{ learnMoreLink: ( - + {LEARN_MORE_LINK} ), @@ -408,7 +405,7 @@ export const Overview: React.FC = () => { defaultMessage="The {externalIdentitiesLink} must be used to configure user access mappings. Read the guide to learn more." values={{ externalIdentitiesLink: ( - + {EXTERNAL_IDENTITIES_LINK} ), @@ -466,7 +463,7 @@ export const Overview: React.FC = () => { - + {LEARN_CUSTOM_FEATURES_BUTTON} @@ -569,7 +566,7 @@ export const Overview: React.FC = () => { defaultMessage="{learnMoreLink} about custom sources." values={{ learnMoreLink: ( - + {LEARN_MORE_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 6b0e43fbce0c4..e37849033a144 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -31,12 +31,12 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../../shared/doc_links'; import { TruncatedContent } from '../../../../shared/truncate'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; -import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; import { SourceContentItem } from '../../../types'; import { NO_CONTENT_MESSAGE, @@ -110,7 +110,7 @@ export const SourceContent: React.FC = () => { defaultMessage="Learn more about adding content in our {documentationLink}" values={{ documentationLink: ( - + {CUSTOM_DOCUMENTATION_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx index 663088f797c18..f741cfdc538fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_layout.tsx @@ -12,11 +12,11 @@ import moment from 'moment'; import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { docLinks } from '../../../../shared/doc_links'; import { PageTemplateProps } from '../../../../shared/layout'; import { AppLogic } from '../../../app_logic'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../../components/layout'; import { NAV } from '../../../constants'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; import { SOURCE_DISABLED_CALLOUT_TITLE, @@ -53,7 +53,7 @@ export const SourceLayout: React.FC = ({ <>

{SOURCE_DISABLED_CALLOUT_DESCRIPTION}

- + {SOURCE_DISABLED_CALLOUT_BUTTON}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx index db4f80dc37f4b..50651bdeb3e75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx @@ -21,10 +21,10 @@ import { } from '@elastic/eui'; import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; +import { docLinks } from '../../../../../shared/doc_links'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV, RESET_BUTTON } from '../../../../constants'; -import { DIFFERENT_SYNC_TYPES_DOCS_URL } from '../../../../routes'; import { LEARN_MORE_LINK, SOURCE_FREQUENCY_DESCRIPTION, @@ -102,7 +102,7 @@ export const Frequency: React.FC = ({ tabId }) => { description={ <> {SOURCE_FREQUENCY_DESCRIPTION}{' '} - + {LEARN_MORE_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx index 2dfa2a6420f7f..460f7e7f42055 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx @@ -22,10 +22,10 @@ import { } from '@elastic/eui'; import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; +import { docLinks } from '../../../../../shared/doc_links'; import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV, RESET_BUTTON } from '../../../../constants'; -import { OBJECTS_AND_ASSETS_DOCS_URL } from '../../../../routes'; import { LEARN_MORE_LINK, SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL, @@ -87,7 +87,7 @@ export const ObjectsAndAssets: React.FC = () => { description={ <> {SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION}{' '} - + {LEARN_MORE_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx index dec275adb3c50..2e777fa906dd6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx @@ -11,9 +11,9 @@ import { useActions, useValues } from 'kea'; import { EuiCallOut, EuiLink, EuiPanel, EuiSwitch, EuiSpacer, EuiText } from '@elastic/eui'; +import { docLinks } from '../../../../../shared/doc_links'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV } from '../../../../constants'; -import { SYNCHRONIZATION_DOCS_URL } from '../../../../routes'; import { LEARN_MORE_LINK, SOURCE_SYNCHRONIZATION_DESCRIPTION, @@ -68,7 +68,7 @@ export const Synchronization: React.FC = () => { description={ <> {SOURCE_SYNCHRONIZATION_DESCRIPTION}{' '} - + {LEARN_MORE_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 687461296ac9e..20a0673709b5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -7,6 +7,8 @@ import { i18n } from '@kbn/i18n'; +import { docLinks } from '../../../shared/doc_links'; + import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; import { ADD_BOX_PATH, @@ -45,23 +47,6 @@ import { EDIT_SLACK_PATH, EDIT_ZENDESK_PATH, EDIT_CUSTOM_PATH, - BOX_DOCS_URL, - CONFLUENCE_DOCS_URL, - CONFLUENCE_SERVER_DOCS_URL, - GITHUB_ENTERPRISE_DOCS_URL, - DROPBOX_DOCS_URL, - GITHUB_DOCS_URL, - GMAIL_DOCS_URL, - GOOGLE_DRIVE_DOCS_URL, - JIRA_DOCS_URL, - JIRA_SERVER_DOCS_URL, - ONEDRIVE_DOCS_URL, - SALESFORCE_DOCS_URL, - SERVICENOW_DOCS_URL, - SHAREPOINT_DOCS_URL, - SLACK_DOCS_URL, - ZENDESK_DOCS_URL, - CUSTOM_SOURCE_DOCS_URL, } from '../../routes'; import { FeatureIds, SourceDataItem } from '../../types'; @@ -75,7 +60,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: BOX_DOCS_URL, + documentationUrl: docLinks.workplaceSearchBox, applicationPortalUrl: 'https://app.box.com/developers/console', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], @@ -104,7 +89,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: true, - documentationUrl: CONFLUENCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchConfluenceCloud, applicationPortalUrl: 'https://developer.atlassian.com/console/myapps/', }, objTypes: [ @@ -138,7 +123,7 @@ export const staticSourceData = [ isPublicKey: true, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: CONFLUENCE_SERVER_DOCS_URL, + documentationUrl: docLinks.workplaceSearchConfluenceServer, }, objTypes: [ SOURCE_OBJ_TYPES.PAGES, @@ -170,7 +155,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: DROPBOX_DOCS_URL, + documentationUrl: docLinks.workplaceSearchDropbox, applicationPortalUrl: 'https://www.dropbox.com/developers/apps', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], @@ -200,7 +185,7 @@ export const staticSourceData = [ hasOauthRedirect: true, needsBaseUrl: false, needsConfiguration: true, - documentationUrl: GITHUB_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGitHub, applicationPortalUrl: 'https://github.com/settings/developers', applicationLinkTitle: GITHUB_LINK_TITLE, }, @@ -242,7 +227,7 @@ export const staticSourceData = [ defaultMessage: 'GitHub Enterprise URL', } ), - documentationUrl: GITHUB_ENTERPRISE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGitHub, applicationPortalUrl: 'https://github.com/settings/developers', applicationLinkTitle: GITHUB_LINK_TITLE, }, @@ -277,7 +262,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: GMAIL_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGmail, applicationPortalUrl: 'https://console.developers.google.com/', }, objTypes: [SOURCE_OBJ_TYPES.EMAILS], @@ -295,7 +280,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: GOOGLE_DRIVE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchGoogleDrive, applicationPortalUrl: 'https://console.developers.google.com/', }, objTypes: [ @@ -328,7 +313,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: true, - documentationUrl: JIRA_DOCS_URL, + documentationUrl: docLinks.workplaceSearchJiraCloud, applicationPortalUrl: 'https://developer.atlassian.com/console/myapps/', }, objTypes: [ @@ -364,7 +349,7 @@ export const staticSourceData = [ isPublicKey: true, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: JIRA_SERVER_DOCS_URL, + documentationUrl: docLinks.workplaceSearchJiraServer, applicationPortalUrl: '', }, objTypes: [ @@ -399,7 +384,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: ONEDRIVE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchOneDrive, applicationPortalUrl: 'https://portal.azure.com/', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.ALL_FILES], @@ -428,7 +413,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SALESFORCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSalesforce, applicationPortalUrl: 'https://salesforce.com/', }, objTypes: [ @@ -464,7 +449,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SALESFORCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSalesforce, applicationPortalUrl: 'https://test.salesforce.com/', }, objTypes: [ @@ -500,7 +485,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: false, needsBaseUrl: true, - documentationUrl: SERVICENOW_DOCS_URL, + documentationUrl: docLinks.workplaceSearchServiceNow, applicationPortalUrl: 'https://www.servicenow.com/my-account/sign-in.html', }, objTypes: [ @@ -533,7 +518,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SHAREPOINT_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSharePoint, applicationPortalUrl: 'https://portal.azure.com/', }, objTypes: [SOURCE_OBJ_TYPES.FOLDERS, SOURCE_OBJ_TYPES.SITES, SOURCE_OBJ_TYPES.ALL_FILES], @@ -562,7 +547,7 @@ export const staticSourceData = [ isPublicKey: false, hasOauthRedirect: true, needsBaseUrl: false, - documentationUrl: SLACK_DOCS_URL, + documentationUrl: docLinks.workplaceSearchSlack, applicationPortalUrl: 'https://api.slack.com/apps/', }, objTypes: [ @@ -585,7 +570,7 @@ export const staticSourceData = [ hasOauthRedirect: true, needsBaseUrl: false, needsSubdomain: true, - documentationUrl: ZENDESK_DOCS_URL, + documentationUrl: docLinks.workplaceSearchZendesk, applicationPortalUrl: 'https://www.zendesk.com/login/', }, objTypes: [SOURCE_OBJ_TYPES.TICKETS], @@ -617,7 +602,7 @@ export const staticSourceData = [ defaultMessage: 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', }), - documentationUrl: CUSTOM_SOURCE_DOCS_URL, + documentationUrl: docLinks.workplaceSearchCustomSources, applicationPortalUrl: '', }, accountContextOnly: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index 8697f10f8afaf..a7c981dad9103 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -24,9 +24,9 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { docLinks } from '../../../shared/doc_links'; import { Loading } from '../../../shared/loading'; import { SourceIcon } from '../../components/shared/source_icon'; -import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../../routes'; import { EXTERNAL_IDENTITIES_LINK, @@ -82,7 +82,7 @@ export const SourcesView: React.FC = ({ children }) => { values={{ addedSourceName, externalIdentitiesLink: ( - + {EXTERNAL_IDENTITIES_LINK} ), @@ -96,7 +96,7 @@ export const SourcesView: React.FC = ({ children }) => { defaultMessage="Documents will not be searchable from Workplace Search until user and group mappings have been configured. {documentPermissionsLink}." values={{ documentPermissionsLink: ( - + {DOCUMENT_PERMISSIONS_LINK} ), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index f7e578b1b4d23..c0362b44b618b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -12,6 +12,7 @@ import { useActions, useValues } from 'kea'; import { EuiSpacer } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { docLinks } from '../../../shared/doc_links'; import { RoleMappingsTable, RoleMappingsHeading, @@ -22,7 +23,6 @@ import { } from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { WorkplaceSearchPageTemplate } from '../../components/layout'; -import { SECURITY_DOCS_URL } from '../../routes'; import { ROLE_MAPPINGS_TABLE_HEADER } from './constants'; @@ -56,7 +56,7 @@ export const RoleMappings: React.FC = () => { const rolesEmptyState = ( ); @@ -65,7 +65,7 @@ export const RoleMappings: React.FC = () => {
initializeRoleMapping()} /> { @@ -100,7 +100,7 @@ export const OauthApplication: React.FC = () => { <> {NON_PLATINUM_OAUTH_DESCRIPTION} - + {EXPLORE_PLATINUM_FEATURES_LINK} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index 009dbffafebd8..3c3a7085d7116 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -12,14 +12,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { docLinks } from '../../../shared/doc_links'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { GETTING_STARTED_DOCS_URL } from '../../routes'; import GettingStarted from './assets/getting_started.png'; -const GETTING_STARTED_LINK_URL = GETTING_STARTED_DOCS_URL; +const GETTING_STARTED_LINK_URL = docLinks.workplaceSearchGettingStarted; export const SetupGuide: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 3070be1e56b5b..c9212bca322d7 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -28,7 +28,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler', + path: '/api/as/v1/engines/:name/crawler', }); }); @@ -61,7 +61,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }); }); @@ -94,7 +94,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/:id', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/:id', }); }); @@ -132,7 +132,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }); }); @@ -165,7 +165,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }); }); @@ -204,7 +204,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/active/cancel', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/active/cancel', }); }); @@ -237,7 +237,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }); }); @@ -293,7 +293,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }); }); @@ -339,7 +339,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }); }); @@ -397,7 +397,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }); }); @@ -435,7 +435,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/crawler/validate_url', + path: '/api/as/v1/crawler/validate_url', }); }); @@ -472,7 +472,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/process_crawls', + path: '/api/as/v1/engines/:name/crawler/process_crawls', }); }); @@ -519,7 +519,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }); }); @@ -556,7 +556,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }); }); @@ -611,7 +611,7 @@ describe('crawler routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index f53b15dadd061..f0fdc5c16098b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -23,7 +23,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler', + path: '/api/as/v1/engines/:name/crawler', }) ); @@ -37,7 +37,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }) ); @@ -52,7 +52,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/:id', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/:id', }) ); @@ -66,7 +66,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests', + path: '/api/as/v1/engines/:name/crawler/crawl_requests', }) ); @@ -80,7 +80,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_requests/active/cancel', + path: '/api/as/v1/engines/:name/crawler/crawl_requests/active/cancel', }) ); @@ -98,7 +98,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }) ); @@ -123,7 +123,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains', + path: '/api/as/v1/engines/:name/crawler/domains', }) ); @@ -138,7 +138,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }) ); @@ -156,7 +156,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }) ); @@ -183,7 +183,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/domains/:id', + path: '/api/as/v1/engines/:name/crawler/domains/:id', }) ); @@ -198,7 +198,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/crawler/validate_url', + path: '/api/as/v1/crawler/validate_url', }) ); @@ -215,7 +215,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/process_crawls', + path: '/api/as/v1/engines/:name/crawler/process_crawls', }) ); @@ -229,7 +229,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }) ); @@ -247,7 +247,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }) ); @@ -261,7 +261,7 @@ export function registerCrawlerRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + path: '/api/as/v1/engines/:name/crawler/crawl_schedule', }) ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts index 018ab433536b2..c3d1468687ec4 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.test.ts @@ -28,7 +28,7 @@ describe('crawler crawl rules routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules', params: { respond_with: 'index', }, @@ -71,7 +71,7 @@ describe('crawler crawl rules routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, @@ -115,7 +115,7 @@ describe('crawler crawl rules routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts index 7c82c73db7263..26637623f0885 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_crawl_rules.ts @@ -29,7 +29,7 @@ export function registerCrawlerCrawlRulesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules', params: { respond_with: 'index', }, @@ -54,7 +54,7 @@ export function registerCrawlerCrawlRulesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, @@ -73,7 +73,7 @@ export function registerCrawlerCrawlRulesRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/crawl_rules/:crawlRuleId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts index 6fb7e99400877..dc7ad493a5149 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts @@ -28,7 +28,7 @@ describe('crawler entry point routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points', params: { respond_with: 'index', }, @@ -69,7 +69,7 @@ describe('crawler entry point routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, @@ -110,7 +110,7 @@ describe('crawler entry point routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts index a6d6fdb24b41f..fd81475c860ad 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts @@ -27,7 +27,7 @@ export function registerCrawlerEntryPointRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points', params: { respond_with: 'index', }, @@ -49,7 +49,7 @@ export function registerCrawlerEntryPointRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, @@ -68,7 +68,7 @@ export function registerCrawlerEntryPointRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts index a37a8311093c7..3d6eb86bcba26 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.test.ts @@ -28,7 +28,7 @@ describe('crawler sitemap routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps', params: { respond_with: 'index', }, @@ -69,7 +69,7 @@ describe('crawler sitemap routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, @@ -110,7 +110,7 @@ describe('crawler sitemap routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts index b63473888eecc..0965acd967306 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_sitemaps.ts @@ -27,7 +27,7 @@ export function registerCrawlerSitemapRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps', params: { respond_with: 'index', }, @@ -49,7 +49,7 @@ export function registerCrawlerSitemapRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, @@ -68,7 +68,7 @@ export function registerCrawlerSitemapRoutes({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', + path: '/api/as/v1/engines/:engineName/crawler/domains/:domainId/sitemaps/:sitemapId', params: { respond_with: 'index', }, diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts index 3e7377477c93e..635345585f925 100644 --- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts @@ -30,12 +30,15 @@ type PreconfiguredAgentPolicyWithDefaultInputs = Omit< package_policies: Array>; }; +export const DEFAULT_SYSTEM_PACKAGE_POLICY_ID = 'default-system-policy'; + export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { name: 'Default policy', namespace: 'default', description: 'Default agent policy created by Kibana', package_policies: [ { + id: DEFAULT_SYSTEM_PACKAGE_POLICY_ID, name: `${FLEET_SYSTEM_PACKAGE}-1`, package: { name: FLEET_SYSTEM_PACKAGE, @@ -47,12 +50,15 @@ export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { monitoring_enabled: monitoringTypes, }; +export const DEFAULT_FLEET_SERVER_POLICY_ID = 'default-fleet-server-policy'; + export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { name: 'Default Fleet Server policy', namespace: 'default', description: 'Default Fleet Server agent policy created by Kibana', package_policies: [ { + id: DEFAULT_FLEET_SERVER_POLICY_ID, name: `${FLEET_SERVER_PACKAGE}-1`, package: { name: FLEET_SERVER_PACKAGE, diff --git a/x-pack/plugins/fleet/common/constants/settings.ts b/x-pack/plugins/fleet/common/constants/settings.ts index 772d938086938..423e71edf10e6 100644 --- a/x-pack/plugins/fleet/common/constants/settings.ts +++ b/x-pack/plugins/fleet/common/constants/settings.ts @@ -6,3 +6,5 @@ */ export const GLOBAL_SETTINGS_SAVED_OBJECT_TYPE = 'ingest_manager_settings'; + +export const GLOBAL_SETTINGS_ID = 'fleet-default-settings'; diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index df484646ef66b..75932fd4a790a 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -56,6 +56,7 @@ export interface PackagePolicyInput extends Omit> & { + id?: string | number; name: string; package: Partial & { name: string }; inputs?: InputsOverride[]; 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 c0914e41872b1..a7dd682384748 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 @@ -348,7 +348,8 @@ export const EditPackagePolicyForm = memo<{ const [formState, setFormState] = useState('INVALID'); const savePackagePolicy = async () => { setFormState('LOADING'); - const result = await sendUpdatePackagePolicy(packagePolicyId, packagePolicy); + const { elasticsearch, ...restPackagePolicy } = packagePolicy; // ignore 'elasticsearch' property since it fails route validation + const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy); setFormState('SUBMITTED'); return result; }; diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index 9853877dc2d61..e0a2a557391df 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -151,8 +151,8 @@ export async function getAuthzFromRequest(req: KibanaRequest): Promise NewPackagePolicy, bumpAgentPolicyRevison = false @@ -803,7 +809,14 @@ export async function addPackageToAgentPolicy( ? transformPackagePolicy(basePackagePolicy) : basePackagePolicy; + // If an ID is provided via preconfiguration, use that value. Otherwise fall back to + // a UUID v5 value seeded from the agent policy's ID and the provided package policy name. + const id = packagePolicyId + ? String(packagePolicyId) + : uuidv5(`${agentPolicy.id}-${packagePolicyName}`, UUID_V5_NAMESPACE); + await packagePolicyService.create(soClient, esClient, newPackagePolicy, { + id, bumpRevision: bumpAgentPolicyRevison, skipEnsureInstalled: true, skipUniqueNameVerification: true, diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 4b87c0957c961..8324079e10da8 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -446,6 +446,7 @@ describe('policy preconfiguration', () => { id: 'test-id', package_policies: [ { + id: 'test-package', package: { name: 'test_package' }, name: 'Test package', }, diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 76fa7778eafa2..84c5c73524f17 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -404,6 +404,7 @@ async function addPreconfiguredPolicyPackages( agentPolicy: AgentPolicy, installedPackagePolicies: Array< Partial> & { + id?: string | number; name: string; installedPackage: Installation; inputs?: InputsOverride[]; @@ -413,7 +414,7 @@ async function addPreconfiguredPolicyPackages( bumpAgentPolicyRevison = false ) { // Add packages synchronously to avoid overwriting - for (const { installedPackage, name, description, inputs } of installedPackagePolicies) { + for (const { installedPackage, id, name, description, inputs } of installedPackagePolicies) { const packageInfo = await getPackageInfo({ savedObjectsClient: soClient, pkgName: installedPackage.name, @@ -427,6 +428,7 @@ async function addPreconfiguredPolicyPackages( agentPolicy, defaultOutput, name, + id, description, (policy) => preconfigurePackageInputs(policy, packageInfo, inputs), bumpAgentPolicyRevison diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 26d581f32d9a2..0e7b7c5e7a093 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -11,6 +11,7 @@ import type { SavedObjectsClientContract } from 'kibana/server'; import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_ID, normalizeHostsForAgents, } from '../../common'; import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common'; @@ -80,10 +81,17 @@ export async function saveSettings( } catch (e) { if (e.isBoom && e.output.statusCode === 404) { const defaultSettings = createDefaultSettings(); - const res = await soClient.create(GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, { - ...defaultSettings, - ...data, - }); + const res = await soClient.create( + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + { + ...defaultSettings, + ...data, + }, + { + id: GLOBAL_SETTINGS_ID, + overwrite: true, + } + ); return { id: res.id, diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 3ba89f1e526b3..64ab8f8ee3a81 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -106,6 +106,7 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( monitoring_output_id: schema.maybe(schema.string()), package_policies: schema.arrayOf( schema.object({ + id: schema.maybe(schema.oneOf([schema.string(), schema.number()])), name: schema.string(), package: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index e26eeadd4edcd..64b8b79d4b2a1 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -140,6 +140,17 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadNodesPluginsResponse = (response?: HttpResponse, error?: any) => { + const status = error ? error.status || 400 : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/nodes/plugins`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadTemplatesResponse, setLoadIndicesResponse, @@ -154,6 +165,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setUpdateIndexSettingsResponse, setSimulateTemplateResponse, setLoadComponentTemplatesResponse, + setLoadNodesPluginsResponse, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 67c9ed067227d..65d3678735689 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -82,6 +82,7 @@ describe('', () => { jest.useFakeTimers(); httpRequestsMockHelpers.setLoadComponentTemplatesResponse(componentTemplates); + httpRequestsMockHelpers.setLoadNodesPluginsResponse([]); // disable all react-beautiful-dnd development warnings (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = true; @@ -296,7 +297,7 @@ describe('', () => { }); describe('mappings (step 4)', () => { - beforeEach(async () => { + const navigateToMappingsStep = async () => { const { actions } = testBed; // Logistics await actions.completeStepOne({ name: TEMPLATE_NAME, indexPatterns: ['index1'] }); @@ -304,6 +305,10 @@ describe('', () => { await actions.completeStepTwo(); // Index settings await actions.completeStepThree('{}'); + }; + + beforeEach(async () => { + await navigateToMappingsStep(); }); it('should set the correct page title', () => { @@ -337,6 +342,43 @@ describe('', () => { expect(find('fieldsListItem').length).toBe(1); }); + + describe('plugin parameters', () => { + const selectMappingsEditorTab = async ( + tab: 'fields' | 'runtimeFields' | 'templates' | 'advanced' + ) => { + const tabIndex = ['fields', 'runtimeFields', 'templates', 'advanced'].indexOf(tab); + const tabElement = testBed.find('mappingsEditor.formTab').at(tabIndex); + await act(async () => { + tabElement.simulate('click'); + }); + testBed.component.update(); + }; + + test('should not render the _size parameter if the mapper size plugin is not installed', async () => { + const { exists } = testBed; + // Navigate to the advanced configuration + await selectMappingsEditorTab('advanced'); + + expect(exists('mappingsEditor.advancedConfiguration.sizeEnabledToggle')).toBe(false); + }); + + test('should render the _size parameter if the mapper size plugin is installed', async () => { + httpRequestsMockHelpers.setLoadNodesPluginsResponse(['mapper-size']); + + await act(async () => { + testBed = await setup(); + }); + testBed.component.update(); + await navigateToMappingsStep(); + + await selectMappingsEditorTab('advanced'); + + expect(testBed.exists('mappingsEditor.advancedConfiguration.sizeEnabledToggle')).toBe( + true + ); + }); + }); }); describe('aliases (step 5)', () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts index 3a8d34c341834..f2fcf7bbab50c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts @@ -339,4 +339,6 @@ export type TestSubjects = | 'versionField' | 'aliasesEditor' | 'settingsEditor' - | 'versionField.input'; + | 'versionField.input' + | 'mappingsEditor.formTab' + | 'mappingsEditor.advancedConfiguration.sizeEnabledToggle'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx index d80712dfa0fea..49922b45f2fde 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/setup_environment.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ComponentType, MemoExoticComponent } from 'react'; import SemVer from 'semver/classes/semver'; /* eslint-disable-next-line @kbn/eslint/no-restricted-paths */ @@ -18,6 +18,7 @@ import { import { MAJOR_VERSION } from '../../../../../../../common'; import { MappingsEditorProvider } from '../../../mappings_editor_context'; import { createKibanaReactContext } from '../../../shared_imports'; +import { Props as MappingsEditorProps } from '../../../mappings_editor'; export const kibanaVersion = new SemVer(MAJOR_VERSION); @@ -82,17 +83,21 @@ const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ }, }); -const defaultProps = { +const defaultProps: MappingsEditorProps = { docLinks: docLinksServiceMock.createStartContract(), + onChange: () => undefined, + esNodesPlugins: [], }; -export const WithAppDependencies = (Comp: any) => (props: any) => - ( - - - - - - - - ); +export const WithAppDependencies = + (Comp: MemoExoticComponent>) => + (props: Partial) => + ( + + + + + + + + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index 5a7c6d439d101..4e4c146c85957 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -10,15 +10,19 @@ import { EuiSpacer } from '@elastic/eui'; import { useForm, Form } from '../../shared_imports'; import { GenericObject, MappingsConfiguration } from '../../types'; +import { MapperSizePluginId } from '../../constants'; import { useDispatch } from '../../mappings_state_context'; import { DynamicMappingSection } from './dynamic_mapping_section'; import { SourceFieldSection } from './source_field_section'; import { MetaFieldSection } from './meta_field_section'; import { RoutingSection } from './routing_section'; +import { MapperSizePluginSection } from './mapper_size_plugin_section'; import { configurationFormSchema } from './configuration_form_schema'; interface Props { value?: MappingsConfiguration; + /** List of plugins installed in the cluster nodes */ + esNodesPlugins: string[]; } const formSerializer = (formData: GenericObject) => { @@ -35,6 +39,7 @@ const formSerializer = (formData: GenericObject) => { sourceField, metaField, _routing, + _size, } = formData; const dynamic = dynamicMappingsEnabled ? true : throwErrorsForUnmappedFields ? 'strict' : false; @@ -47,6 +52,7 @@ const formSerializer = (formData: GenericObject) => { _source: sourceField, _meta: metaField, _routing, + _size, }; return serialized; @@ -67,6 +73,8 @@ const formDeserializer = (formData: GenericObject) => { }, _meta, _routing, + // For the Mapper Size plugin + _size, } = formData; return { @@ -84,10 +92,11 @@ const formDeserializer = (formData: GenericObject) => { }, metaField: _meta ?? {}, _routing, + _size, }; }; -export const ConfigurationForm = React.memo(({ value }: Props) => { +export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) => { const isMounted = useRef(false); const { form } = useForm({ @@ -100,6 +109,9 @@ export const ConfigurationForm = React.memo(({ value }: Props) => { const dispatch = useDispatch(); const { subscribe, submit, reset, getFormData } = form; + const isMapperSizeSectionVisible = + value?._size !== undefined || esNodesPlugins.includes(MapperSizePluginId); + useEffect(() => { const subscription = subscribe(({ data, isValid, validate }) => { dispatch({ @@ -150,6 +162,7 @@ export const ConfigurationForm = React.memo(({ value }: Props) => { + {isMapperSizeSectionVisible && } ); }); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index b5fa5f25b865b..d8e3e8d5ae7c2 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -192,4 +192,12 @@ export const configurationFormSchema: FormSchema = { defaultValue: false, }, }, + _size: { + enabled: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.sizeLabel', { + defaultMessage: 'Index the _source field size in bytes', + }), + defaultValue: false, + }, + }, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/mapper_size_plugin_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/mapper_size_plugin_section.tsx new file mode 100644 index 0000000000000..db2ded2e09990 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/mapper_size_plugin_section.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiCode } from '@elastic/eui'; + +import { documentationService } from '../../../../services/documentation'; +import { UseField, FormRow, ToggleField } from '../../shared_imports'; + +export const MapperSizePluginSection = () => { + return ( + + {i18n.translate('xpack.idxMgmt.mappingsEditor.sizeDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + _source: _source, + }} + /> + } + > + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx index 701cd86510b5d..a875b9985a8f4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_from_json_button.tsx @@ -13,10 +13,12 @@ import { LoadMappingsProvider } from './load_mappings_provider'; interface Props { onJson(json: { [key: string]: any }): void; + /** List of plugins installed in the cluster nodes */ + esNodesPlugins: string[]; } -export const LoadMappingsFromJsonButton = ({ onJson }: Props) => ( - +export const LoadMappingsFromJsonButton = ({ onJson, esNodesPlugins }: Props) => ( + {(openModal) => ( {i18n.translate('xpack.idxMgmt.mappingsEditor.loadFromJsonButtonLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx index 2413df5e5d64d..8259c78b8e140 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.test.tsx @@ -22,7 +22,7 @@ import { registerTestBed, TestBed } from '@kbn/test/jest'; import { LoadMappingsProvider } from './load_mappings_provider'; const ComponentToTest = ({ onJson }: { onJson: () => void }) => ( - + {(openModal) => (
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 84c7722ca1b88..13f9df1739005 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -406,7 +406,7 @@ export function LayerPanel( defaultMessage: 'Requires field', }); - const isOptional = !group.required; + const isOptional = !group.required && !group.suggestedValue; return ( - {' '} { ).toHaveLength(0); }); + test('when metric value isStaticValue', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'date-column', + operation: { + isBucketed: true, + dataType: 'date', + scale: 'interval', + label: 'Date', + }, + }, + { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number', + scale: 'ratio', + label: 'Metric', + isStaticValue: true, + }, + }, + { + columnId: 'group-column', + operation: { + isBucketed: true, + dataType: 'string', + scale: 'ratio', + label: 'Group', + }, + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as HeatmapVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([]); + }); + test('when there are 3 or more buckets', () => { expect( getSuggestions({ diff --git a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts index ebe93419edce6..aeddb8473fa98 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/suggestions.ts @@ -19,9 +19,10 @@ export const getSuggestions: Visualization['getSugges keptLayerIds, }) => { if ( - state?.shape === CHART_SHAPES.HEATMAP && - (state.xAccessor || state.yAccessor || state.valueAccessor) && - table.changeType !== 'extended' + (state?.shape === CHART_SHAPES.HEATMAP && + (state.xAccessor || state.yAccessor || state.valueAccessor) && + table.changeType !== 'extended') || + table.columns.some((col) => col.operation.isStaticValue) ) { return []; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index f4d3e11c30cc3..bf645599cae11 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -58,7 +58,7 @@ function getAxisName(axis: 'x' | 'y') { } export const isBucketed = (op: OperationMetadata) => op.isBucketed && op.scale === 'ordinal'; -const isNumericMetric = (op: OperationMetadata) => op.dataType === 'number'; +const isNumericMetric = (op: OperationMetadata) => op.dataType === 'number' && !op.isStaticValue; export const filterOperationsAxis = (op: OperationMetadata) => isBucketed(op) || op.scale === 'interval'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts index 08361490cdc2c..fcc9a57285ba6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/get_drop_props.ts @@ -10,6 +10,7 @@ import { isDraggedOperation, DraggedOperation, DropType, + VisualizationDimensionGroupConfig, } from '../../../types'; import { getOperationDisplay } from '../../operations'; import { hasField, isDraggedField } from '../../utils'; @@ -36,7 +37,8 @@ const operationLabels = getOperationDisplay(); export function getNewOperation( field: IndexPatternField | undefined | false, filterOperations: (meta: OperationMetadata) => boolean, - targetColumn: GenericIndexPatternColumn + targetColumn: GenericIndexPatternColumn, + prioritizedOperation?: GenericIndexPatternColumn['operationType'] ) { if (!field) { return; @@ -47,7 +49,12 @@ export function getNewOperation( } // Detects if we can change the field only, otherwise change field + operation const shouldOperationPersist = targetColumn && newOperations.includes(targetColumn.operationType); - return shouldOperationPersist ? targetColumn.operationType : newOperations[0]; + if (shouldOperationPersist) { + return targetColumn.operationType; + } + const existsPrioritizedOperation = + prioritizedOperation && newOperations.includes(prioritizedOperation); + return existsPrioritizedOperation ? prioritizedOperation : newOperations[0]; } export function getField( @@ -85,7 +92,7 @@ export function getDropProps(props: GetDropProps) { } else if (hasTheSameField(sourceColumn, targetColumn)) { return; } else if (filterOperations(sourceColumn)) { - return getDropPropsForCompatibleGroup(targetColumn); + return getDropPropsForCompatibleGroup(props.dimensionGroups, dragging.columnId, targetColumn); } else { return getDropPropsFromIncompatibleGroup({ ...props, dragging }); } @@ -137,12 +144,26 @@ function getDropPropsForSameGroup(targetColumn?: GenericIndexPatternColumn): Dro return targetColumn ? { dropTypes: ['reorder'] } : { dropTypes: ['duplicate_compatible'] }; } -function getDropPropsForCompatibleGroup(targetColumn?: GenericIndexPatternColumn): DropProps { - return { +function getDropPropsForCompatibleGroup( + dimensionGroups: VisualizationDimensionGroupConfig[], + sourceId: string, + targetColumn?: GenericIndexPatternColumn +): DropProps { + const canSwap = + targetColumn && + dimensionGroups + .find((group) => group.accessors.some((accessor) => accessor.columnId === sourceId)) + ?.filterOperations(targetColumn); + + const dropTypes: DropProps = { dropTypes: targetColumn - ? ['replace_compatible', 'replace_duplicate_compatible', 'swap_compatible'] + ? ['replace_compatible', 'replace_duplicate_compatible'] : ['move_compatible', 'duplicate_compatible'], }; + if (canSwap) { + dropTypes.dropTypes.push('swap_compatible'); + } + return dropTypes; } function getDropPropsFromIncompatibleGroup({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index b518f667a0bfb..0c538d0fc9486 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -70,10 +70,19 @@ function onFieldDrop(props: DropHandlerProps) { dimensionGroups, } = props; + const prioritizedOperation = dimensionGroups.find( + (g) => g.groupId === groupId + )?.prioritizedOperation; + const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; const targetColumn = layer.columns[columnId]; - const newOperation = getNewOperation(droppedItem.field, filterOperations, targetColumn); + const newOperation = getNewOperation( + droppedItem.field, + filterOperations, + targetColumn, + prioritizedOperation + ); if (!isDraggedField(droppedItem) || !newOperation) { return false; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index da5e39c907d07..d7ea174718813 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1250,6 +1250,7 @@ describe('IndexPattern Data Source', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isStaticValue: false, } as Operation); }); @@ -1723,6 +1724,7 @@ describe('IndexPattern Data Source', () => { ...state.layers.first.columns, newStatic: { dataType: 'number', + isStaticValue: true, isBucketed: false, label: 'Static value: 0', operationType: 'static_value', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 402371930b93e..6179f34226125 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -68,12 +68,13 @@ export function columnToOperation( column: GenericIndexPatternColumn, uniqueLabel?: string ): Operation { - const { dataType, label, isBucketed, scale } = column; + const { dataType, label, isBucketed, scale, operationType } = column; return { dataType: normalizeOperationDataType(dataType), isBucketed, scale, label: uniqueLabel || label, + isStaticValue: operationType === 'static_value', }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index a821dcee29d6d..783314968633f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1188,6 +1188,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -1197,6 +1198,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Count of records', scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1273,6 +1275,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -1282,6 +1285,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Count of records', scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1546,6 +1550,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: undefined, + isStaticValue: false, }, }, ], @@ -1568,6 +1573,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: undefined, + isStaticValue: false, }, }, ], @@ -1614,6 +1620,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, scale: 'interval', + isStaticValue: false, }, }, { @@ -1623,6 +1630,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1683,6 +1691,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: 'ordinal', + isStaticValue: false, }, }, { @@ -1692,6 +1701,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, scale: 'interval', + isStaticValue: false, }, }, { @@ -1701,6 +1711,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1780,6 +1791,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'string', isBucketed: true, scale: 'ordinal', + isStaticValue: false, }, }, { @@ -1789,6 +1801,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'date', isBucketed: true, scale: 'interval', + isStaticValue: false, }, }, { @@ -1798,6 +1811,7 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: false, }, }, ], @@ -1900,6 +1914,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'My Custom Range', scale: 'ordinal', + isStaticValue: false, }, }, { @@ -1909,6 +1924,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'timestampLabel', scale: 'interval', + isStaticValue: false, }, }, { @@ -1918,6 +1934,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Unique count of dest', scale: undefined, + isStaticValue: false, }, }, ], @@ -2429,6 +2446,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'My Op', scale: undefined, + isStaticValue: false, }, }, { @@ -2438,6 +2456,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'Top 5', scale: undefined, + isStaticValue: false, }, }, ], @@ -2501,6 +2520,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: 'timestampLabel', scale: 'interval', + isStaticValue: false, }, }, { @@ -2510,6 +2530,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Cumulative sum of Records label', scale: undefined, + isStaticValue: false, }, }, { @@ -2519,6 +2540,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: 'Cumulative sum of (incomplete)', scale: undefined, + isStaticValue: false, }, }, ], @@ -2580,6 +2602,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: true, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -2589,6 +2612,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, { @@ -2598,6 +2622,7 @@ describe('IndexPattern Data Source suggestions', () => { isBucketed: false, label: '', scale: undefined, + isStaticValue: false, }, }, ], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 3e56565b2e13e..6d9a39887b940 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -72,6 +72,7 @@ describe('static_value', () => { dataType: 'number', isBucketed: false, operationType: 'static_value', + isStaticValue: true, references: [], params: { value: '23', @@ -106,6 +107,7 @@ describe('static_value', () => { dataType: 'number', isBucketed: false, operationType: 'static_value', + isStaticValue: true, references: [], params: { value: '23', @@ -237,6 +239,7 @@ describe('static_value', () => { label: 'Static value', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '100' }, @@ -253,6 +256,7 @@ describe('static_value', () => { label: 'Static value', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -263,6 +267,7 @@ describe('static_value', () => { label: 'Static value: 23', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -283,6 +288,7 @@ describe('static_value', () => { label: 'Static value: 23', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -300,6 +306,7 @@ describe('static_value', () => { label: 'Static value', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '23' }, @@ -312,6 +319,7 @@ describe('static_value', () => { label: 'Static value: 53', dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { value: '53' }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 45a35d18873fc..0adaf8ea00640 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -81,6 +81,7 @@ export const staticValueOperation: OperationDefinition< dataType: 'number', isBucketed: false, scale: 'ratio', + isStaticValue: true, }; }, toExpression: (layer, columnId) => { @@ -122,6 +123,7 @@ export const staticValueOperation: OperationDefinition< label: ofName(previousParams.value), dataType: 'number', operationType: 'static_value', + isStaticValue: true, isBucketed: false, scale: 'ratio', params: { ...previousParams, value: String(previousParams.value ?? defaultValue) }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 08136ed501cfc..b2cae54b0f8ba 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -378,10 +378,6 @@ describe('getOperationTypesForField', () => { "operationType": "formula", "type": "managedReference", }, - Object { - "operationType": "static_value", - "type": "managedReference", - }, ], }, Object { @@ -398,6 +394,20 @@ describe('getOperationTypesForField', () => { }, ], }, + Object { + "operationMetaData": Object { + "dataType": "number", + "isBucketed": false, + "isStaticValue": true, + "scale": "ratio", + }, + "operations": Array [ + Object { + "operationType": "static_value", + "type": "managedReference", + }, + ], + }, ] `); }); diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index 80bdb8ce737b0..d0539b99b8eab 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -15,8 +15,8 @@ import type { LensUnwrapResult, LensByReferenceInput, } from './embeddable/embeddable'; -import { SavedObjectIndexStore } from './persistence'; -import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; +import { SavedObjectIndexStore, checkForDuplicateTitle } from './persistence'; +import { OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { DOC_TYPE } from '../common/constants'; export type LensAttributeService = AttributeService< diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 4c92864776045..b9a79963510ed 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -22,7 +22,8 @@ import { } from '../../../../../src/plugins/charts/public'; import { AutoScale } from './auto_scale'; import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder, getContrastColor } from '../shared_components'; +import { getContrastColor } from '../shared_components'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { LensIconChartMetric } from '../assets/chart_metric'; import type { FormatFactory } from '../../common'; import type { MetricChartProps } from '../../common/expressions'; diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts index 8fceffa0db1fe..7c0f8dd073674 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.test.ts @@ -31,6 +31,18 @@ describe('metric_suggestions', () => { }; } + function staticValueCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + label: `Static value: ${columnId}`, + isBucketed: false, + isStaticValue: true, + }, + }; + } + function dateCol(columnId: string): TableSuggestionColumn { return { columnId, @@ -86,7 +98,19 @@ describe('metric_suggestions', () => { ).map((table) => expect(getSuggestions({ table, keptLayerIds: ['l1'] })).toEqual([])) ); }); + test('does not suggest for a static value', () => { + const suggestion = getSuggestions({ + table: { + columns: [staticValueCol('id')], + isMultiRow: false, + layerId: 'l1', + changeType: 'unchanged', + }, + keptLayerIds: [], + }); + expect(suggestion).toHaveLength(0); + }); test('suggests a basic metric chart', () => { const [suggestion, ...rest] = getSuggestions({ table: { diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts index 3d6b2683b4ad2..e8a377169bb97 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts @@ -26,7 +26,8 @@ export function getSuggestions({ keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || table.columns.length !== 1 || - table.columns[0].operation.dataType !== 'number' + table.columns[0].operation.dataType !== 'number' || + table.columns[0].operation.isStaticValue ) { return []; } diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 857bfa676faf4..87e51378377aa 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -90,7 +90,7 @@ export const getMetricVisualization = ({ defaultMessage: 'Metric', }), groupLabel: i18n.translate('xpack.lens.metric.groupLabel', { - defaultMessage: 'Single value', + defaultMessage: 'Goal and single value', }), sortPriority: 3, }, diff --git a/x-pack/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts index 66f75aed35fcc..0fd3388ef416a 100644 --- a/x-pack/plugins/lens/public/persistence/index.ts +++ b/x-pack/plugins/lens/public/persistence/index.ts @@ -7,3 +7,4 @@ export * from './saved_object_store'; export * from './filter_references'; +export { checkForDuplicateTitle } from './saved_objects_utils'; diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts new file mode 100644 index 0000000000000..d9d29a6fdb2d7 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/check_for_duplicate_title.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { OverlayStart, SavedObjectsClientContract } from 'kibana/public'; +import type { SavedObject } from 'src/plugins/saved_objects/public'; +import { SAVE_DUPLICATE_REJECTED } from './constants'; +import { findObjectByTitle } from './find_object_by_title'; +import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; + +/** + * check for an existing SavedObject with the same title in ES + * returns Promise when it's no duplicate, or the modal displaying the warning + * that's there's a duplicate is confirmed, else it returns a rejected Promise + */ +export async function checkForDuplicateTitle( + savedObject: Pick< + SavedObject, + 'id' | 'title' | 'getDisplayName' | 'lastSavedTitle' | 'copyOnSave' | 'getEsType' + >, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: (() => void) | undefined, + services: { savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart } +): Promise { + const { savedObjectsClient, overlays } = services; + + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { + return true; + } + + const duplicate = await findObjectByTitle( + savedObjectsClient, + savedObject.getEsType(), + savedObject.title + ); + + if (!duplicate || duplicate.id === savedObject.id) { + return true; + } + + if (onTitleDuplicate) { + onTitleDuplicate(); + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } + + // TODO: make onTitleDuplicate a required prop and remove UI components from this class + // Need to leave here until all users pass onTitleDuplicate. + return displayDuplicateTitleConfirmModal(savedObject, overlays); +} diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/confirm_modal_promise.tsx b/x-pack/plugins/lens/public/persistence/saved_objects_utils/confirm_modal_promise.tsx new file mode 100644 index 0000000000000..120be6b66b339 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/confirm_modal_promise.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { OverlayStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal } from '@elastic/eui'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +export function confirmModalPromise( + message = '', + title = '', + confirmBtnText = '', + overlays: OverlayStart +): Promise { + return new Promise((resolve, reject) => { + const cancelButtonText = i18n.translate('xpack.lens.confirmModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }); + + const modal = overlays.openModal( + toMountPoint( + { + modal.close(); + reject(); + }} + onConfirm={() => { + modal.close(); + resolve(true); + }} + confirmButtonText={confirmBtnText} + cancelButtonText={cancelButtonText} + title={title} + > + {message} + + ) + ); + }); +} diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/constants.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/constants.ts new file mode 100644 index 0000000000000..8b4e26d2ed8e5 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +/** An error message to be used when the user rejects a confirm save with duplicate title. */ +export const SAVE_DUPLICATE_REJECTED = i18n.translate( + 'xpack.lens.saveDuplicateRejectedDescription', + { + defaultMessage: 'Save with duplicate title confirmation was rejected', + } +); diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts new file mode 100644 index 0000000000000..f40224e42923c --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/display_duplicate_title_confirm_modal.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { OverlayStart } from 'kibana/public'; +import type { SavedObject } from 'src/plugins/saved_objects/public'; +import { SAVE_DUPLICATE_REJECTED } from './constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +export function displayDuplicateTitleConfirmModal( + savedObject: Pick, + overlays: OverlayStart +): Promise { + const confirmMessage = i18n.translate( + 'xpack.lens.confirmModal.saveDuplicateConfirmationMessage', + { + defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, + values: { title: savedObject.title, name: savedObject.getDisplayName() }, + } + ); + + const confirmButtonText = i18n.translate('xpack.lens.confirmModal.saveDuplicateButtonLabel', { + defaultMessage: 'Save {name}', + values: { name: savedObject.getDisplayName() }, + }); + try { + return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays); + } catch { + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } +} diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.test.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.test.ts new file mode 100644 index 0000000000000..72d8dcafcf348 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { findObjectByTitle } from './find_object_by_title'; +import { SimpleSavedObject, SavedObjectsClientContract, SavedObject } from 'src/core/public'; + +describe('findObjectByTitle', () => { + const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; + + beforeEach(() => { + savedObjectsClient.find = jest.fn(); + }); + + it('returns undefined if title is not provided', async () => { + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', ''); + expect(match).toBeUndefined(); + }); + + it('matches any case', async () => { + const indexPattern = new SimpleSavedObject(savedObjectsClient, { + attributes: { title: 'foo' }, + } as SavedObject); + savedObjectsClient.find = jest.fn().mockImplementation(() => + Promise.resolve({ + savedObjects: [indexPattern], + }) + ); + const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', 'FOO'); + expect(match).toEqual(indexPattern); + }); +}); diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts new file mode 100644 index 0000000000000..93c86eeb5f3b0 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/find_object_by_title.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectsClientContract, + SimpleSavedObject, + SavedObjectAttributes, +} from 'kibana/public'; + +/** Returns an object matching a given title */ +export async function findObjectByTitle( + savedObjectsClient: SavedObjectsClientContract, + type: string, + title: string +): Promise | void> { + if (!title) { + return; + } + + // Elastic search will return the most relevant results first, which means exact matches should come + // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. + const response = await savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + return response.savedObjects.find( + (obj) => obj.get('title').toLowerCase() === title.toLowerCase() + ); +} diff --git a/x-pack/plugins/lens/public/persistence/saved_objects_utils/index.ts b/x-pack/plugins/lens/public/persistence/saved_objects_utils/index.ts new file mode 100644 index 0000000000000..37f36f53951c7 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/saved_objects_utils/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { checkForDuplicateTitle } from './check_for_duplicate_title'; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index 55b621498bb10..ef160b1dd682b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -20,7 +20,7 @@ import type { LensMultiTable } from '../../common'; import type { PieExpressionArgs } from '../../common/expressions'; import { PieComponent } from './render_function'; import { VisualizationContainer } from '../visualization_container'; -import { EmptyPlaceholder } from '../shared_components'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { LensIconChartDonut } from '../assets/chart_donut'; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 3b9fdaf094822..4bf7fcf9f8925 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -36,7 +36,7 @@ import { byDataColorPaletteMap, extractUniqTermsMap, } from './render_helpers'; -import { EmptyPlaceholder } from '../shared_components'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import './visualization.scss'; import { ChartsPluginSetup, @@ -86,6 +86,7 @@ export function PieComponent( truncateLegend, hideLabels, palette, + showValuesInLegend, } = props.args; const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); @@ -315,7 +316,7 @@ export function PieComponent( (legend.getShowLegendDefault?.(bucketColumns) ?? false))) } flatLegend={legend.flat} - showLegendExtra={legend.showValues} + showLegendExtra={showValuesInLegend} legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} onElementClick={props.interactive ?? true ? onElementClickHandler : undefined} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index d86500ff8a4fa..bcd9d79babbab 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -14,8 +14,10 @@ import { byDataColorPaletteMap, extractUniqTermsMap, checkTableForContainsSmallValues, + shouldShowValuesInLegend, } from './render_helpers'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import type { PieLayerState } from '../../common/expressions'; describe('render helpers', () => { describe('#getSliceValue', () => { @@ -374,4 +376,28 @@ describe('render helpers', () => { expect(checkTableForContainsSmallValues(datatable, columnId, 1)).toBeFalsy(); }); }); + + describe('#shouldShowValuesInLegend', () => { + it('should firstly read the state value', () => { + expect( + shouldShowValuesInLegend({ showValuesInLegend: true } as PieLayerState, 'waffle') + ).toBeTruthy(); + + expect( + shouldShowValuesInLegend({ showValuesInLegend: false } as PieLayerState, 'waffle') + ).toBeFalsy(); + }); + + it('should read value from meta in case of value in state is undefined', () => { + expect( + shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'waffle') + ).toBeTruthy(); + + expect(shouldShowValuesInLegend({} as PieLayerState, 'waffle')).toBeTruthy(); + + expect( + shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'pie') + ).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index fa20eb6f20fa8..a9685e13e1774 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -8,8 +8,9 @@ import type { Datum, LayerValue } from '@elastic/charts'; import type { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import type { LensFilterEvent } from '../types'; -import type { PieChartTypes } from '../../common/expressions/pie_chart/types'; +import type { PieChartTypes, PieLayerState } from '../../common/expressions/pie_chart/types'; import type { PaletteDefinition, PaletteOutput } from '../../../../../src/plugins/charts/public'; +import { PartitionChartsMeta } from './partition_charts_meta'; export function getSliceValue(d: Datum, metricColumn: DatatableColumn) { const value = d[metricColumn.id]; @@ -44,6 +45,14 @@ export const isPartitionShape = (shape: PieChartTypes | string) => export const isTreemapOrMosaicShape = (shape: PieChartTypes | string) => ['treemap', 'mosaic'].includes(shape); +export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTypes) => { + if ('showValues' in PartitionChartsMeta[shape]?.legend) { + return layer.showValuesInLegend ?? PartitionChartsMeta[shape]?.legend?.showValues ?? true; + } + + return false; +}; + export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) => [...new Set(dataTable.rows.map((item) => item[columnId]))].reduce( (acc, item, index) => ({ diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index a2e3f6d3ca865..229ef9b387ac0 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -288,6 +288,35 @@ describe('suggestions', () => { ).toHaveLength(0); }); + it('should reject when metric value isStaticValue', () => { + const results = suggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + { + columnId: 'a', + operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, + }, + { + columnId: 'e', + operation: { + label: 'Count', + dataType: 'number' as DataType, + isBucketed: false, + isStaticValue: true, + }, + }, + ], + changeType: 'initial', + }, + state: undefined, + keptLayerIds: ['first'], + }); + + expect(results.length).toEqual(0); + }); + it('should hide suggestions when there are no buckets', () => { const currentSuggestions = suggestions({ table: { diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index 248f4a82b1694..dd42dd6474e0b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -27,7 +27,8 @@ function shouldReject({ table, keptLayerIds, state }: SuggestionRequest 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]) || table.changeType === 'reorder' || - shouldRejectIntervals + shouldRejectIntervals || + table.columns.some((col) => col.operation.isStaticValue) ); } diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index e13fbf62708ee..57270337e67a4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -5,10 +5,12 @@ * 2.0. */ -import { Ast } from '@kbn/interpreter/common'; -import { PaletteRegistry } from 'src/plugins/charts/public'; -import { Operation, DatasourcePublicAPI } from '../types'; +import type { Ast } from '@kbn/interpreter/common'; +import type { PaletteRegistry } from 'src/plugins/charts/public'; +import type { Operation, DatasourcePublicAPI } from '../types'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; +import { shouldShowValuesInLegend } from './render_helpers'; + import type { PieVisualizationState } from '../../common/expressions'; export function toExpression( @@ -34,6 +36,7 @@ function expressionHelper( const operations = layer.groups .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); + if (!layer.metric || !operations.length) { return null; } @@ -55,6 +58,7 @@ function expressionHelper( categoryDisplay: [layer.categoryDisplay], legendDisplay: [layer.legendDisplay], legendPosition: [layer.legendPosition || 'right'], + showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], percentDecimals: [ state.shape === 'waffle' ? DEFAULT_PERCENT_DECIMALS diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 195a72cca9fed..70ad4d8c07daa 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -6,7 +6,7 @@ */ import './toolbar.scss'; -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -23,6 +23,7 @@ import type { PieVisualizationState, SharedPieLayerState } from '../../common/ex import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types'; import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components'; import { PalettePicker } from '../shared_components'; +import { shouldShowValuesInLegend } from './render_helpers'; const legendOptions: Array<{ value: SharedPieLayerState['legendDisplay']; @@ -55,6 +56,67 @@ const legendOptions: Array<{ export function PieToolbar(props: VisualizationToolbarProps) { const { state, setState } = props; const layer = state.layers[0]; + + const onStateChange = useCallback( + (part: Record) => { + setState({ + ...state, + layers: [{ ...layer, ...part }], + }); + }, + [layer, state, setState] + ); + + const onCategoryDisplayChange = useCallback( + (option) => onStateChange({ categoryDisplay: option }), + [onStateChange] + ); + + const onNumberDisplayChange = useCallback( + (option) => onStateChange({ numberDisplay: option }), + [onStateChange] + ); + + const onPercentDecimalsChange = useCallback( + (option) => { + onStateChange({ percentDecimals: option }); + }, + [onStateChange] + ); + + const onLegendDisplayChange = useCallback( + (optionId) => { + onStateChange({ legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value }); + }, + [onStateChange] + ); + + const onLegendPositionChange = useCallback( + (id) => onStateChange({ legendPosition: id as Position }), + [onStateChange] + ); + + const onNestedLegendChange = useCallback( + (id) => onStateChange({ nestedLegend: !layer.nestedLegend }), + [layer, onStateChange] + ); + + const onTruncateLegendChange = useCallback(() => { + const current = layer.truncateLegend ?? true; + onStateChange({ truncateLegend: !current }); + }, [layer, onStateChange]); + + const onLegendMaxLinesChange = useCallback( + (val) => onStateChange({ legendMaxLines: val }), + [onStateChange] + ); + + const onValueInLegendChange = useCallback(() => { + onStateChange({ + showValuesInLegend: !shouldShowValuesInLegend(layer, state.shape), + }); + }, [layer, state.shape, onStateChange]); + if (!layer) { return null; } @@ -87,12 +149,7 @@ export function PieToolbar(props: VisualizationToolbarProps { - setState({ - ...state, - layers: [{ ...layer, categoryDisplay: option }], - }); - }} + onChange={onCategoryDisplayChange} /> ) : null} @@ -110,12 +167,7 @@ export function PieToolbar(props: VisualizationToolbarProps { - setState({ - ...state, - layers: [{ ...layer, numberDisplay: option }], - }); - }} + onChange={onNumberDisplayChange} />
) : null} @@ -131,59 +183,28 @@ export function PieToolbar(props: VisualizationToolbarProps { - setState({ - ...state, - layers: [{ ...layer, percentDecimals: value }], - }); - }} + setValue={onPercentDecimalsChange} /> { - setState({ - ...state, - layers: [ - { - ...layer, - legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value, - }, - ], - }); - }} + onDisplayChange={onLegendDisplayChange} + valueInLegend={shouldShowValuesInLegend(layer, state.shape)} + renderValueInLegendSwitch={ + 'showValues' in PartitionChartsMeta[state.shape]?.legend ?? false + } + onValueInLegendChange={onValueInLegendChange} position={layer.legendPosition} - onPositionChange={(id) => { - setState({ - ...state, - layers: [{ ...layer, legendPosition: id as Position }], - }); - }} + onPositionChange={onLegendPositionChange} renderNestedLegendSwitch nestedLegend={!!layer.nestedLegend} - onNestedLegendChange={() => { - setState({ - ...state, - layers: [{ ...layer, nestedLegend: !layer.nestedLegend }], - }); - }} + onNestedLegendChange={onNestedLegendChange} shouldTruncate={layer.truncateLegend ?? true} - onTruncateLegendChange={() => { - const current = layer.truncateLegend ?? true; - setState({ - ...state, - layers: [{ ...layer, truncateLegend: !current }], - }); - }} + onTruncateLegendChange={onTruncateLegendChange} maxLines={layer?.legendMaxLines} - onMaxLinesChange={(val) => { - setState({ - ...state, - layers: [{ ...layer, legendMaxLines: val }], - }); - }} + onMaxLinesChange={onLegendMaxLinesChange} /> ); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 49a80b73da1c4..e7c5e2f78920b 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -41,7 +41,7 @@ function newLayerState(layerId: string): PieLayerState { const bucketedOperations = (op: OperationMetadata) => op.isBucketed; const numberMetricOperations = (op: OperationMetadata) => - !op.isBucketed && op.dataType === 'number'; + !op.isBucketed && op.dataType === 'number' && !op.isStaticValue; const applyPaletteToColumnConfig = ( columns: AccessorConfig[], diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b89492d7e7588..ecf237ac1327d 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -57,6 +57,7 @@ import type { PieVisualizationPluginSetupPlugins, } from './pie_visualization'; import type { HeatmapVisualization as HeatmapVisualizationType } from './heatmap_visualization'; +import type { GaugeVisualization as GaugeVisualizationType } from './visualizations/gauge'; import type { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; import { AppNavLinkStatus } from '../../../../src/core/public'; @@ -169,6 +170,7 @@ export class LensPlugin { private metricVisualization: MetricVisualizationType | undefined; private pieVisualization: PieVisualizationType | undefined; private heatmapVisualization: HeatmapVisualizationType | undefined; + private gaugeVisualization: GaugeVisualizationType | undefined; private stopReportManager?: () => void; @@ -308,6 +310,7 @@ export class LensPlugin { MetricVisualization, PieVisualization, HeatmapVisualization, + GaugeVisualization, } = await import('./async_services'); this.datatableVisualization = new DatatableVisualization(); this.editorFrameService = new EditorFrameService(); @@ -316,6 +319,7 @@ export class LensPlugin { this.metricVisualization = new MetricVisualization(); this.pieVisualization = new PieVisualization(); this.heatmapVisualization = new HeatmapVisualization(); + this.gaugeVisualization = new GaugeVisualization(); const editorFrameSetupInterface = this.editorFrameService.setup(); @@ -337,6 +341,7 @@ export class LensPlugin { this.metricVisualization.setup(core, dependencies); this.pieVisualization.setup(core, dependencies); this.heatmapVisualization.setup(core, dependencies); + this.gaugeVisualization.setup(core, dependencies); } start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { diff --git a/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx b/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx deleted file mode 100644 index ab69c6cc3139d..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/empty_placeholder.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -const noResultsMessage = ( - -); - -export const EmptyPlaceholder = ({ - icon, - message = noResultsMessage, -}: { - icon: IconType; - message?: JSX.Element; -}) => ( - <> - - - -

{message}

-
- -); diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 9ffddaa1a135b..dd2d5aa7c8558 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './empty_placeholder'; export type { ToolbarPopoverProps } from './toolbar_popover'; export { ToolbarPopover } from './toolbar_popover'; export { LegendSettingsPopover } from './legend_settings_popover'; @@ -17,3 +16,4 @@ export * from './helpers'; export { LegendActionPopover } from './legend_action_popover'; export { ValueLabelsSettings } from './value_labels_settings'; export * from './static_header'; +export * from './vis_label'; diff --git a/x-pack/plugins/lens/public/shared_components/vis_label.tsx b/x-pack/plugins/lens/public/shared_components/vis_label.tsx new file mode 100644 index 0000000000000..2fec7f56561a9 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/vis_label.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +type LabelMode = 'auto' | 'custom' | 'none'; + +interface Label { + mode: LabelMode; + label: string; +} + +export interface VisLabelProps { + label: string; + mode: LabelMode; + handleChange: (label: Label) => void; + placeholder?: string; + hasAutoOption?: boolean; + header?: string; + dataTestSubj?: string; +} + +const defaultHeader = i18n.translate('xpack.lens.label.header', { + defaultMessage: 'Label', +}); + +const MODE_NONE = { + id: `lns_title_none`, + value: 'none', + text: i18n.translate('xpack.lens.chart.labelVisibility.none', { + defaultMessage: 'None', + }), +}; + +const MODE_CUSTOM = { + id: `lns_title_custom`, + value: 'custom', + text: i18n.translate('xpack.lens.chart.labelVisibility.custom', { + defaultMessage: 'Custom', + }), +}; + +const MODE_AUTO = { + id: `lns_title_auto`, + value: 'auto', + text: i18n.translate('xpack.lens.chart.labelVisibility.auto', { + defaultMessage: 'Auto', + }), +}; + +const modeDefaultOptions = [MODE_NONE, MODE_CUSTOM]; + +const modeEnhancedOptions = [MODE_NONE, MODE_AUTO, MODE_CUSTOM]; + +export function VisLabel({ + label, + mode, + handleChange, + hasAutoOption = false, + placeholder = '', + header = defaultHeader, + dataTestSubj, +}: VisLabelProps) { + return ( + + + { + if (target.value === 'custom') { + handleChange({ label: '', mode: target.value as LabelMode }); + return; + } + handleChange({ label: '', mode: target.value as LabelMode }); + }} + options={hasAutoOption ? modeEnhancedOptions : modeDefaultOptions} + value={mode} + /> + + + handleChange({ mode: 'custom', label: target.value })} + aria-label={header} + /> + + + ); +} diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index af9897581fcf4..67b7ccac97478 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -616,7 +616,9 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { visualizationState, framePublicAPI: { // any better idea to avoid `as`? - activeData: state.activeData as TableInspectorAdapter, + activeData: state.activeData + ? (current(state.activeData) as TableInspectorAdapter) + : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), }, activeVisualization, @@ -653,7 +655,9 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => { visualizationState: state.visualization.state, framePublicAPI: { // any better idea to avoid `as`? - activeData: state.activeData as TableInspectorAdapter, + activeData: state.activeData + ? (current(state.activeData) as TableInspectorAdapter) + : undefined, datasourceLayers: getDatasourceLayers(state.datasourceStates, datasourceMap), }, activeVisualization, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index da1db7727aff7..8c5331100e903 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -217,6 +217,7 @@ export interface Datasource { props: DatasourceDimensionDropProps & { groupId: string; dragging: DragContextState['dragging']; + prioritizedOperation?: string; } ) => { dropTypes: DropType[]; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; @@ -431,6 +432,8 @@ export interface OperationMetadata { // TODO currently it's not possible to differentiate between a field from a raw // document and an aggregated metric which might be handy in some cases. Once we // introduce a raw document datasource, this should be considered here. + + isStaticValue?: boolean; } export interface VisualizationConfigProps { @@ -475,6 +478,8 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { required?: boolean; requiredMinDimensionCount?: number; dataTestSubj?: string; + prioritizedOperation?: string; + suggestedValue?: () => number | undefined; /** * When the dimension editor is enabled for this group, all dimensions in the group diff --git a/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap new file mode 100644 index 0000000000000..b588c1d341a75 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/__snapshots__/chart_component.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GaugeComponent renders the chart 1`] = ` + + + + +`; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx new file mode 100644 index 0000000000000..517c5718f613f --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.test.tsx @@ -0,0 +1,429 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Chart, Goal } from '@elastic/charts'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import type { ColorStop, LensMultiTable } from '../../../common'; +import { fieldFormatsServiceMock } from '../../../../../../src/plugins/field_formats/public/mocks'; +import { GaugeArguments, GaugeLabelMajorMode } from '../../../common/expressions/gauge_chart'; +import { GaugeComponent, GaugeRenderProps } from './chart_component'; +import { DatatableColumn, DatatableRow } from 'src/plugins/expressions/common'; +import { VisualizationContainer } from '../../visualization_container'; + +jest.mock('@elastic/charts', () => { + const original = jest.requireActual('@elastic/charts'); + + return { + ...original, + getSpecId: jest.fn(() => {}), + }; +}); + +const numberColumn = (id = 'metric-accessor'): DatatableColumn => ({ + id, + name: 'Count of records', + meta: { + type: 'number', + index: 'kibana_sample_data_ecommerce', + params: { + id: 'number', + }, + }, +}); + +const createData = ( + row: DatatableRow = { 'metric-accessor': 3, 'min-accessor': 0, 'max-accessor': 10 } +): LensMultiTable => { + return { + type: 'lens_multitable', + tables: { + layerId: { + type: 'datatable', + rows: [row], + columns: Object.keys(row).map((key) => numberColumn(key)), + }, + }, + }; +}; + +const chartsThemeService = chartPluginMock.createSetupContract().theme; +const palettesRegistry = chartPluginMock.createPaletteRegistry(); +const formatService = fieldFormatsServiceMock.createStartContract(); +const args: GaugeArguments = { + labelMajor: 'Gauge', + description: 'vis description', + metricAccessor: 'metric-accessor', + minAccessor: '', + maxAccessor: '', + goalAccessor: '', + shape: 'verticalBullet', + colorMode: 'none', + ticksPosition: 'auto', + labelMajorMode: 'auto', +}; + +describe('GaugeComponent', function () { + let wrapperProps: GaugeRenderProps; + + beforeAll(() => { + wrapperProps = { + data: createData(), + chartsThemeService, + args, + paletteService: palettesRegistry, + formatFactory: formatService.deserialize, + }; + }); + + it('renders the chart', () => { + const component = shallowWithIntl(); + expect(component.find(Chart)).toMatchSnapshot(); + }); + + it('shows empty placeholder when metricAccessor is not provided', async () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: undefined, + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'min-accessor': 0, 'max-accessor': 10 }), + }; + const component = shallowWithIntl(); + expect(component.find(VisualizationContainer)).toHaveLength(1); + }); + + it('shows empty placeholder when minimum accessor equals maximum accessor', async () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'metric-accessor': 0, 'min-accessor': 0, 'max-accessor': 0 }), + }; + const component = shallowWithIntl(); + expect(component.find('EmptyPlaceholder')).toHaveLength(1); + }); + it('shows empty placeholder when minimum accessor value is greater maximum accessor value', async () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'metric-accessor': 0, 'min-accessor': 0, 'max-accessor': -10 }), + }; + const component = shallowWithIntl(); + expect(component.find('EmptyPlaceholder')).toHaveLength(1); + }); + it('when metric value is bigger than max, it takes maximum value', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + ticksPosition: 'bands', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + data: createData({ 'metric-accessor': 12, 'min-accessor': 0, 'max-accessor': 10 }), + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('actual')).toEqual(10); + }); + + describe('labelMajor and labelMinor settings', () => { + it('displays no labelMajor and no labelMinor when no passed', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + labelMajorMode: 'none' as GaugeLabelMajorMode, + labelMinor: '', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('labelMajor')).toEqual(''); + expect(goal.prop('labelMinor')).toEqual(''); + }); + it('displays custom labelMajor and labelMinor when passed', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + labelMajorMode: 'custom' as GaugeLabelMajorMode, + labelMajor: 'custom labelMajor', + labelMinor: 'custom labelMinor', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('labelMajor')).toEqual('custom labelMajor '); + expect(goal.prop('labelMinor')).toEqual('custom labelMinor '); + }); + it('displays auto labelMajor', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + labelMajorMode: 'auto' as GaugeLabelMajorMode, + labelMajor: '', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('labelMajor')).toEqual('Count of records '); + }); + }); + + describe('ticks and color bands', () => { + it('displays auto ticks', () => { + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + }; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 3.33, 6.67, 10]); + }); + it('spreads auto ticks only over the [min, max] domain if color bands defined bigger domain', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [10, 20, 30] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 20, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 3.33, 6.67, 10]); + }); + it('sets proper color bands and ticks on color bands for values smaller than maximum', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [1, 2, 3] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 1, 2, 3, 10]); + expect(goal.prop('bands')).toEqual([0, 1, 2, 3, 10]); + }); + it('sets proper color bands and ticks on color bands if palette steps are smaller than minimum', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [-10, -5, 0] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 10]); + expect(goal.prop('bands')).toEqual([0, 10]); + }); + it('sets proper color bands and ticks on color bands if percent palette steps are smaller than 0', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [-20, -60, 80], + range: 'percent', + rangeMin: 0, + rangeMax: 4, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 8, 10]); + expect(goal.prop('bands')).toEqual([0, 8, 10]); + }); + it('doesnt set ticks for values differing <10%', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [1, 1.5, 3] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 10, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 1, 3, 10]); + expect(goal.prop('bands')).toEqual([0, 1, 1.5, 3, 10]); + }); + it('sets proper color bands and ticks on color bands for values greater than maximum', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [10, 20, 30, 31] as unknown as ColorStop[], + range: 'number', + rangeMin: 0, + rangeMax: 30, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + palette, + ticksPosition: 'bands', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 10]); + expect(goal.prop('bands')).toEqual([0, 10]); + }); + it('passes number bands from color palette with no stops defined', () => { + const palette = { + type: 'palette' as const, + name: 'gray', + params: { + colors: ['#aaa', '#bbb'], + gradient: false, + stops: [], + range: 'number', + rangeMin: 0, + rangeMax: 10, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + colorMode: 'palette', + palette, + ticksPosition: 'bands', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 5, 10]); + expect(goal.prop('bands')).toEqual([0, 5, 10]); + }); + it('passes percent bands from color palette', () => { + const palette = { + type: 'palette' as const, + name: 'custom', + params: { + colors: ['#aaa', '#bbb', '#ccc'], + gradient: false, + stops: [20, 60, 80], + range: 'percent', + rangeMin: 0, + rangeMax: 10, + }, + }; + const customProps = { + ...wrapperProps, + args: { + ...wrapperProps.args, + colorMode: 'palette', + palette, + ticksPosition: 'bands', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }, + } as GaugeRenderProps; + const goal = shallowWithIntl().find(Goal); + expect(goal.prop('ticks')).toEqual([0, 2, 6, 8, 10]); + expect(goal.prop('bands')).toEqual([0, 2, 6, 8, 10]); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx new file mode 100644 index 0000000000000..a8f2b0e1c204c --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/chart_component.tsx @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { Chart, Goal, Settings } from '@elastic/charts'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { + CustomPaletteState, + ChartsPluginSetup, + PaletteRegistry, +} from 'src/plugins/charts/public'; +import { VisualizationContainer } from '../../visualization_container'; +import './index.scss'; +import { LensIconChartGaugeHorizontal, LensIconChartGaugeVertical } from '../../assets/chart_gauge'; +import { EmptyPlaceholder } from '../../../../../../src/plugins/charts/public'; +import { getMaxValue, getMinValue, getValueFromAccessor } from './utils'; +import { + GaugeExpressionProps, + GaugeShapes, + GaugeTicksPosition, + GaugeTicksPositions, + GaugeLabelMajorMode, +} from '../../../common/expressions/gauge_chart'; +import type { FormatFactory } from '../../../common'; + +export type GaugeRenderProps = GaugeExpressionProps & { + formatFactory: FormatFactory; + chartsThemeService: ChartsPluginSetup['theme']; + paletteService: PaletteRegistry; +}; + +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + +function normalizeColors({ colors, stops, range }: CustomPaletteState, min: number) { + if (!colors) { + return; + } + const colorsOutOfRangeSmaller = Math.max( + stops.filter((stop, i) => (range === 'percent' ? stop < 0 : stop < min)).length, + 0 + ); + return colors.slice(colorsOutOfRangeSmaller); +} + +function normalizeBands( + { colors, stops, range }: CustomPaletteState, + { min, max }: { min: number; max: number } +) { + if (!stops.length) { + const step = (max - min) / colors.length; + return [min, ...colors.map((_, i) => min + (i + 1) * step)]; + } + if (range === 'percent') { + const filteredStops = stops.filter((stop) => stop >= 0 && stop <= 100); + return [min, ...filteredStops.map((step) => min + step * ((max - min) / 100)), max]; + } + const orderedStops = stops.filter((stop, i) => stop < max && stop > min); + return [min, ...orderedStops, max]; +} + +function getTitle( + labelMajorMode: GaugeLabelMajorMode, + labelMajor?: string, + fallbackTitle?: string +) { + if (labelMajorMode === 'none') { + return ''; + } else if (labelMajorMode === 'auto') { + return `${fallbackTitle || ''} `; + } + return `${labelMajor || fallbackTitle || ''} `; +} + +// TODO: once charts handle not displaying labels when there's no space for them, it's safe to remove this +function getTicksLabels(baseStops: number[]) { + const tenPercentRange = (Math.max(...baseStops) - Math.min(...baseStops)) * 0.1; + const lastIndex = baseStops.length - 1; + return baseStops.filter((stop, i) => { + if (i === 0 || i === lastIndex) { + return true; + } + + return !( + stop - baseStops[i - 1] < tenPercentRange || baseStops[lastIndex] - stop < tenPercentRange + ); + }); +} + +function getTicks( + ticksPosition: GaugeTicksPosition, + range: [number, number], + colorBands?: number[] +) { + if (ticksPosition === GaugeTicksPositions.bands && colorBands) { + return colorBands && getTicksLabels(colorBands); + } + const TICKS_NO = 3; + const min = Math.min(...(colorBands || []), ...range); + const max = Math.max(...(colorBands || []), ...range); + const step = (max - min) / TICKS_NO; + return [ + ...Array(TICKS_NO) + .fill(null) + .map((_, i) => Number((min + step * i).toFixed(2))), + max, + ]; +} + +export const GaugeComponent: FC = ({ + data, + args, + formatFactory, + chartsThemeService, +}) => { + const { + shape: subtype, + metricAccessor, + palette, + colorMode, + labelMinor, + labelMajor, + labelMajorMode, + ticksPosition, + } = args; + if (!metricAccessor) { + return ; + } + + const chartTheme = chartsThemeService.useChartsTheme(); + + const table = Object.values(data.tables)[0]; + const metricColumn = table.columns.find((col) => col.id === metricAccessor); + + const chartData = table.rows.filter( + (v) => typeof v[metricAccessor!] === 'number' || Array.isArray(v[metricAccessor!]) + ); + const row = chartData?.[0]; + + const metricValue = getValueFromAccessor('metricAccessor', row, args); + + const icon = + subtype === GaugeShapes.horizontalBullet + ? LensIconChartGaugeHorizontal + : LensIconChartGaugeVertical; + + if (typeof metricValue !== 'number') { + return ; + } + + const goal = getValueFromAccessor('goalAccessor', row, args); + const min = getMinValue(row, args); + const max = getMaxValue(row, args); + + if (min === max) { + return ( + + } + /> + ); + } else if (min > max) { + return ( + + } + /> + ); + } + + const tickFormatter = formatFactory( + metricColumn?.meta?.params?.params + ? metricColumn?.meta?.params + : { + id: 'number', + params: { + pattern: max - min > 5 ? `0,0` : `0,0.0`, + }, + } + ); + const colors = palette?.params?.colors ? normalizeColors(palette.params, min) : undefined; + const bands: number[] = (palette?.params as CustomPaletteState) + ? normalizeBands(args.palette?.params as CustomPaletteState, { min, max }) + : [min, max]; + + // TODO: format in charts + const formattedActual = Math.round(Math.min(Math.max(metricValue, min), max) * 1000) / 1000; + + return ( + + + = min && goal <= max ? goal : undefined} + actual={formattedActual} + tickValueFormatter={({ value: tickValue }) => tickFormatter.convert(tickValue)} + bands={bands} + ticks={getTicks(ticksPosition, [min, max], bands)} + bandFillColor={ + colorMode === 'palette' && colors + ? (val) => { + const index = bands && bands.indexOf(val.value) - 1; + return colors && index >= 0 && colors[index] + ? colors[index] + : colors[colors.length - 1]; + } + : () => `rgba(255,255,255,0)` + } + labelMajor={getTitle(labelMajorMode, labelMajor, metricColumn?.name)} + labelMinor={labelMinor ? labelMinor + ' ' : ''} + /> + + ); +}; + +export function GaugeChartReportable(props: GaugeRenderProps) { + return ( + + + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/constants.ts b/x-pack/plugins/lens/public/visualizations/gauge/constants.ts new file mode 100644 index 0000000000000..1a801dc942652 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LENS_GAUGE_RENDERER = 'lens_gauge_renderer'; +export const LENS_GAUGE_ID = 'lnsGauge'; + +export const GROUP_ID = { + METRIC: 'metric', + MIN: 'min', + MAX: 'max', + GOAL: 'goal', +} as const; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss new file mode 100644 index 0000000000000..d7664b9d2da16 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.scss @@ -0,0 +1,3 @@ +.lnsDynamicColoringRow { + align-items: center; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx new file mode 100644 index 0000000000000..89a4be3300e2e --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/dimension_editor.tsx @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiColorPaletteDisplay, + EuiFormRow, + EuiFlexItem, + EuiSwitchEvent, + EuiSwitch, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { + isNumericFieldForDatatable, + GaugeVisualizationState, + GaugeTicksPositions, + GaugeColorModes, +} from '../../../common/expressions'; +import { + applyPaletteParams, + CustomizablePalette, + CUSTOM_PALETTE, + FIXED_PROGRESSION, + getStopsForFixedMode, + PalettePanelContainer, +} from '../../shared_components/'; +import type { VisualizationDimensionEditorProps } from '../../types'; +import { defaultPaletteParams } from './palette_config'; + +import './dimension_editor.scss'; +import { getMaxValue, getMinValue } from './utils'; + +export function GaugeDimensionEditor( + props: VisualizationDimensionEditorProps & { + paletteService: PaletteRegistry; + } +) { + const { state, setState, frame, accessor } = props; + const [isPaletteOpen, setIsPaletteOpen] = useState(false); + + if (state?.metricAccessor !== accessor) return null; + + const currentData = frame.activeData?.[state.layerId]; + const [firstRow] = currentData?.rows || []; + + if (accessor == null || firstRow == null || !isNumericFieldForDatatable(currentData, accessor)) { + return null; + } + + const hasDynamicColoring = state?.colorMode === 'palette'; + + const currentMinMax = { + min: getMinValue(firstRow, state), + max: getMaxValue(firstRow, state), + }; + + const activePalette = state?.palette || { + type: 'palette', + name: defaultPaletteParams.name, + params: { + ...defaultPaletteParams, + colorStops: undefined, + stops: undefined, + rangeMin: currentMinMax.min, + rangeMax: (currentMinMax.max * 3) / 4, + }, + }; + + const displayStops = applyPaletteParams(props.paletteService, activePalette, currentMinMax); + + const togglePalette = () => setIsPaletteOpen(!isPaletteOpen); + return ( + <> + + { + const { checked } = e.target; + const params = checked + ? { + palette: { + ...activePalette, + params: { + ...activePalette.params, + stops: displayStops, + }, + }, + ticksPosition: GaugeTicksPositions.bands, + colorMode: GaugeColorModes.palette, + } + : { + ticksPosition: GaugeTicksPositions.auto, + colorMode: GaugeColorModes.none, + }; + + setState({ + ...state, + ...params, + }); + }} + /> + + {hasDynamicColoring && ( + <> + + + + color) + } + type={FIXED_PROGRESSION} + onClick={togglePalette} + /> + + + + {i18n.translate('xpack.lens.paletteTableGradient.customize', { + defaultMessage: 'Edit', + })} + + + { + // if the new palette is not custom, replace the rangeMin with the artificial one + if ( + newPalette.name !== CUSTOM_PALETTE && + newPalette.params && + newPalette.params.rangeMin !== currentMinMax.min + ) { + newPalette.params.rangeMin = currentMinMax.min; + } + setState({ + ...state, + palette: newPalette, + }); + }} + /> + + + + + + { + setState({ + ...state, + ticksPosition: + state.ticksPosition === GaugeTicksPositions.bands + ? GaugeTicksPositions.auto + : GaugeTicksPositions.bands, + }); + }} + /> + + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx b/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx new file mode 100644 index 0000000000000..b8852f22691ed --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/expression.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n-react'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import type { IInterpreterRenderHandlers } from '../../../../../../src/plugins/expressions'; +import type { FormatFactory } from '../../../common'; +import { LENS_GAUGE_RENDERER } from './constants'; +import type { + ChartsPluginSetup, + PaletteRegistry, +} from '../../../../../../src/plugins/charts/public'; +import { GaugeChartReportable } from './chart_component'; +import type { GaugeExpressionProps } from '../../../common/expressions'; + +export const getGaugeRenderer = (dependencies: { + formatFactory: FormatFactory; + chartsThemeService: ChartsPluginSetup['theme']; + paletteService: PaletteRegistry; +}) => ({ + name: LENS_GAUGE_RENDERER, + displayName: i18n.translate('xpack.lens.gauge.visualizationName', { + defaultMessage: 'Gauge', + }), + help: '', + validate: () => undefined, + reuseDomNode: true, + render: async ( + domNode: Element, + config: GaugeExpressionProps, + handlers: IInterpreterRenderHandlers + ) => { + ReactDOM.render( + + { + + } + , + domNode, + () => { + handlers.done(); + } + ); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}); + +const MemoizedChart = React.memo(GaugeChartReportable); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts b/x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts new file mode 100644 index 0000000000000..231b6bacbbe20 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/gauge_visualization.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './expression'; +export * from './visualization'; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/index.scss b/x-pack/plugins/lens/public/visualizations/gauge/index.scss new file mode 100644 index 0000000000000..c999fe7e218a2 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/index.scss @@ -0,0 +1,14 @@ +.lnsGaugeExpression__container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + text-align: center; + overflow-x: hidden; + + .echChart { + width: 100%; + } +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/index.ts b/x-pack/plugins/lens/public/visualizations/gauge/index.ts new file mode 100644 index 0000000000000..b0a4f26f2d675 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup } from 'kibana/public'; +import type { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import type { EditorFrameSetup } from '../../types'; +import type { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public'; +import type { FormatFactory } from '../../../common'; +import { transparentizePalettes } from './utils'; + +export interface GaugeVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; + charts: ChartsPluginSetup; +} + +export class GaugeVisualization { + setup( + core: CoreSetup, + { expressions, formatFactory, editorFrame, charts }: GaugeVisualizationPluginSetupPlugins + ) { + editorFrame.registerVisualization(async () => { + const { getGaugeVisualization, getGaugeRenderer } = await import('../../async_services'); + const palettes = transparentizePalettes(await charts.palettes.getPalettes()); + + expressions.registerRenderer( + getGaugeRenderer({ + formatFactory, + chartsThemeService: charts.theme, + paletteService: palettes, + }) + ); + return getGaugeVisualization({ paletteService: palettes }); + }); + } +} diff --git a/x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx b/x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx new file mode 100644 index 0000000000000..20e026a3f5719 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/palette_config.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequiredPaletteParamTypes } from '../../../common'; +import { defaultPaletteParams as sharedDefaultParams } from '../../shared_components/'; + +export const DEFAULT_PALETTE_NAME = 'gray'; +export const DEFAULT_COLOR_STEPS = 3; +export const DEFAULT_MIN_STOP = 0; +export const DEFAULT_MAX_STOP = 100; + +export const defaultPaletteParams: RequiredPaletteParamTypes = { + ...sharedDefaultParams, + rangeMin: DEFAULT_MIN_STOP, + rangeMax: DEFAULT_MAX_STOP, + name: DEFAULT_PALETTE_NAME, + steps: DEFAULT_COLOR_STEPS, + maxSteps: 5, +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts new file mode 100644 index 0000000000000..cced4bb2c309b --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSuggestions } from './suggestions'; +import { GaugeShapes, GaugeVisualizationState } from '../../../common/expressions'; +import { layerTypes } from '../../../common'; + +const metricColumn = { + columnId: 'metric-column', + operation: { + isBucketed: false, + dataType: 'number' as const, + scale: 'ratio' as const, + label: 'Metric', + }, +}; + +const bucketColumn = { + columnId: 'date-column-01', + operation: { + isBucketed: true, + dataType: 'date' as const, + scale: 'interval' as const, + label: 'Date', + }, +}; + +describe('gauge suggestions', () => { + describe('rejects suggestions', () => { + test('when currently active and unchanged data', () => { + const unchangedSuggestion = { + table: { + layerId: 'first', + isMultiRow: true, + columns: [], + changeType: 'unchanged' as const, + }, + state: { + shape: GaugeShapes.horizontalBullet, + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }; + expect(getSuggestions(unchangedSuggestion)).toHaveLength(0); + }); + test('when there are buckets', () => { + const bucketAndMetricSuggestion = { + table: { + layerId: 'first', + isMultiRow: true, + columns: [bucketColumn, metricColumn], + changeType: 'initial' as const, + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }; + expect(getSuggestions(bucketAndMetricSuggestion)).toEqual([]); + }); + test('when currently active with partial configuration', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [metricColumn], + changeType: 'initial', + }, + state: { + shape: GaugeShapes.horizontalBullet, + layerId: 'first', + layerType: layerTypes.DATA, + minAccessor: 'some-field', + labelMajorMode: 'auto', + ticksPosition: 'auto', + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toHaveLength(0); + }); + test('for tables with a single bucket dimension', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [bucketColumn], + changeType: 'reduced', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([]); + }); + test('when two metric accessor are available', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [ + metricColumn, + { + ...metricColumn, + columnId: 'metric-column2', + }, + ], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([]); + }); + }); +}); + +describe('shows suggestions', () => { + test('when complete configuration has been resolved', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [metricColumn], + changeType: 'initial', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + } as GaugeVisualizationState, + keptLayerIds: ['first'], + }) + ).toEqual([ + { + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: GaugeShapes.horizontalBullet, + metricAccessor: 'metric-column', + labelMajorMode: 'auto', + ticksPosition: 'auto', + }, + title: 'Gauge', + hide: true, + previewIcon: 'empty', + score: 0.5, + }, + { + hide: true, + previewIcon: 'empty', + title: 'Gauge', + score: 0.5, + state: { + layerId: 'first', + layerType: 'data', + metricAccessor: 'metric-column', + shape: GaugeShapes.verticalBullet, + ticksPosition: 'auto', + labelMajorMode: 'auto', + }, + }, + ]); + }); + test('passes the state when change is shape change', () => { + expect( + getSuggestions({ + table: { + layerId: 'first', + isMultiRow: true, + columns: [metricColumn], + changeType: 'extended', + }, + state: { + layerId: 'first', + layerType: layerTypes.DATA, + shape: GaugeShapes.horizontalBullet, + metricAccessor: 'metric-column', + } as GaugeVisualizationState, + keptLayerIds: ['first'], + subVisualizationId: GaugeShapes.verticalBullet, + }) + ).toEqual([ + { + state: { + layerType: layerTypes.DATA, + shape: GaugeShapes.verticalBullet, + metricAccessor: 'metric-column', + labelMajorMode: 'auto', + ticksPosition: 'auto', + layerId: 'first', + }, + previewIcon: 'empty', + title: 'Gauge', + hide: false, // shows suggestion when current is gauge + score: 0.5, + }, + ]); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts new file mode 100644 index 0000000000000..03198411581c1 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { TableSuggestion, Visualization } from '../../types'; +import { layerTypes } from '../../../common'; +import { + GaugeShape, + GaugeShapes, + GaugeTicksPositions, + GaugeLabelMajorModes, + GaugeVisualizationState, +} from '../../../common/expressions/gauge_chart'; + +const isNotNumericMetric = (table: TableSuggestion) => + table.columns?.[0]?.operation.dataType !== 'number' || + table.columns.some((col) => col.operation.isBucketed); + +const hasLayerMismatch = (keptLayerIds: string[], table: TableSuggestion) => + keptLayerIds.length > 1 || (keptLayerIds.length && table.layerId !== keptLayerIds[0]); + +export const getSuggestions: Visualization['getSuggestions'] = ({ + table, + state, + keptLayerIds, + subVisualizationId, +}) => { + const isGauge = Boolean( + state && (state.minAccessor || state.maxAccessor || state.goalAccessor || state.metricAccessor) + ); + + const numberOfAccessors = + state && + [state.minAccessor, state.maxAccessor, state.goalAccessor, state.metricAccessor].filter(Boolean) + .length; + + if ( + hasLayerMismatch(keptLayerIds, table) || + isNotNumericMetric(table) || + (!isGauge && table.columns.length > 1) || + (isGauge && (numberOfAccessors !== table.columns.length || table.changeType === 'initial')) + ) { + return []; + } + + const shape: GaugeShape = + state?.shape === GaugeShapes.verticalBullet + ? GaugeShapes.verticalBullet + : GaugeShapes.horizontalBullet; + + const baseSuggestion = { + state: { + ...state, + shape, + layerId: table.layerId, + layerType: layerTypes.DATA, + ticksPosition: GaugeTicksPositions.auto, + labelMajorMode: GaugeLabelMajorModes.auto, + }, + title: i18n.translate('xpack.lens.gauge.gaugeLabel', { + defaultMessage: 'Gauge', + }), + previewIcon: 'empty', + score: 0.5, + hide: !isGauge || state?.metricAccessor === undefined, // only display for gauges for beta + }; + + const suggestions = isGauge + ? [ + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + ...state, + shape: + state?.shape === GaugeShapes.verticalBullet + ? GaugeShapes.horizontalBullet + : GaugeShapes.verticalBullet, + }, + }, + ] + : [ + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + metricAccessor: table.columns[0].columnId, + }, + }, + { + ...baseSuggestion, + state: { + ...baseSuggestion.state, + metricAccessor: table.columns[0].columnId, + shape: + state?.shape === GaugeShapes.verticalBullet + ? GaugeShapes.horizontalBullet + : GaugeShapes.verticalBullet, + }, + }, + ]; + + return suggestions; +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss new file mode 100644 index 0000000000000..893ed71235881 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_config_panel.scss @@ -0,0 +1,3 @@ +.lnsGaugeToolbar__popover { + width: 500px; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx new file mode 100644 index 0000000000000..2d3d54be97453 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/gauge_toolbar.test.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FormEvent } from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { GaugeToolbar } from '.'; +import { FramePublicAPI, VisualizationToolbarProps } from '../../../types'; +import { ToolbarButton } from 'src/plugins/kibana_react/public'; +import { ReactWrapper } from 'enzyme'; +import { GaugeVisualizationState } from '../../../../common/expressions'; +import { act } from 'react-dom/test-utils'; + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +class Harness { + wrapper: ReactWrapper; + + constructor(wrapper: ReactWrapper) { + this.wrapper = wrapper; + } + + togglePopover() { + this.wrapper.find(ToolbarButton).simulate('click'); + } + + public get titleLabel() { + return this.wrapper.find('EuiFieldText[data-test-subj="lnsToolbarGaugeLabelMajor"]'); + } + public get titleSelect() { + return this.wrapper.find('EuiSelect[data-test-subj="lnsToolbarGaugeLabelMajor-select"]'); + } + + modifyTitle(e: FormEvent) { + act(() => { + this.titleLabel.prop('onChange')!(e); + }); + } + + public get subtitleSelect() { + return this.wrapper.find('EuiSelect[data-test-subj="lnsToolbarGaugeLabelMinor-select"]'); + } + + public get subtitleLabel() { + return this.wrapper.find('EuiFieldText[data-test-subj="lnsToolbarGaugeLabelMinor"]'); + } + + modifySubtitle(e: FormEvent) { + act(() => { + this.subtitleLabel.prop('onChange')!(e); + }); + } +} + +describe('gauge toolbar', () => { + let harness: Harness; + let defaultProps: VisualizationToolbarProps; + + beforeEach(() => { + defaultProps = { + setState: jest.fn(), + frame: {} as FramePublicAPI, + state: { + layerId: 'layerId', + layerType: 'data', + metricAccessor: 'metric-accessor', + minAccessor: '', + maxAccessor: '', + goalAccessor: '', + shape: 'verticalBullet', + colorMode: 'none', + ticksPosition: 'auto', + labelMajorMode: 'auto', + }, + }; + }); + + it('should reflect state in the UI for default props', async () => { + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.titleLabel.prop('value')).toBe(''); + expect(harness.titleSelect.prop('value')).toBe('auto'); + expect(harness.subtitleLabel.prop('value')).toBe(''); + expect(harness.subtitleSelect.prop('value')).toBe('none'); + }); + it('should reflect state in the UI for non-default props', async () => { + const props = { + ...defaultProps, + state: { + ...defaultProps.state, + ticksPosition: 'bands' as const, + labelMajorMode: 'custom' as const, + labelMajor: 'new labelMajor', + labelMinor: 'new labelMinor', + }, + }; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.titleLabel.prop('value')).toBe('new labelMajor'); + expect(harness.titleSelect.prop('value')).toBe('custom'); + expect(harness.subtitleLabel.prop('value')).toBe('new labelMinor'); + expect(harness.subtitleSelect.prop('value')).toBe('custom'); + }); + + describe('labelMajor', () => { + it('labelMajor label is disabled if labelMajor is selected to be none', () => { + defaultProps.state.labelMajorMode = 'none' as const; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.titleSelect.prop('value')).toBe('none'); + expect(harness.titleLabel.prop('disabled')).toBe(true); + expect(harness.titleLabel.prop('value')).toBe(''); + }); + it('labelMajor mode switches to custom when user starts typing', () => { + defaultProps.state.labelMajorMode = 'auto' as const; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.titleSelect.prop('value')).toBe('auto'); + expect(harness.titleLabel.prop('disabled')).toBe(false); + expect(harness.titleLabel.prop('value')).toBe(''); + harness.modifyTitle({ target: { value: 'labelMajor' } } as unknown as FormEvent); + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + labelMajorMode: 'custom', + labelMajor: 'labelMajor', + }) + ); + }); + }); + describe('labelMinor', () => { + it('labelMinor label is enabled if labelMinor is string', () => { + defaultProps.state.labelMinor = 'labelMinor label'; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.subtitleSelect.prop('value')).toBe('custom'); + expect(harness.subtitleLabel.prop('disabled')).toBe(false); + expect(harness.subtitleLabel.prop('value')).toBe('labelMinor label'); + }); + it('labelMajor mode can switch to custom', () => { + defaultProps.state.labelMinor = ''; + + harness = new Harness(mountWithIntl()); + harness.togglePopover(); + + expect(harness.subtitleSelect.prop('value')).toBe('none'); + expect(harness.subtitleLabel.prop('disabled')).toBe(true); + expect(harness.subtitleLabel.prop('value')).toBe(''); + harness.modifySubtitle({ target: { value: 'labelMinor label' } } as unknown as FormEvent); + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + labelMinor: 'labelMinor label', + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx new file mode 100644 index 0000000000000..e907dc0529019 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/toolbar_component/index.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { VisualizationToolbarProps } from '../../../types'; +import { ToolbarPopover, useDebouncedValue, VisLabel } from '../../../shared_components'; +import './gauge_config_panel.scss'; +import { GaugeLabelMajorMode, GaugeVisualizationState } from '../../../../common/expressions'; + +export const GaugeToolbar = memo((props: VisualizationToolbarProps) => { + const { state, setState, frame } = props; + const metricDimensionTitle = + state.layerId && + frame.activeData?.[state.layerId]?.columns.find((col) => col.id === state.metricAccessor)?.name; + + const [subtitleMode, setSubtitleMode] = useState(() => + state.labelMinor ? 'custom' : 'none' + ); + + const { inputValue, handleInputChange } = useDebouncedValue({ + onChange: setState, + value: state, + }); + + return ( + + + + { + setSubtitleMode(inputValue.labelMinor ? 'custom' : 'none'); + }} + title={i18n.translate('xpack.lens.gauge.appearanceLabel', { + defaultMessage: 'Appearance', + })} + type="visualOptions" + buttonDataTestSubj="lnsVisualOptionsButton" + panelClassName="lnsGaugeToolbar__popover" + > + + { + handleInputChange({ + ...inputValue, + labelMajor: value.label, + labelMajorMode: value.mode, + }); + }} + /> + + + { + handleInputChange({ + ...inputValue, + labelMinor: value.label, + }); + setSubtitleMode(value.mode); + }} + /> + + + + + + ); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.test.ts new file mode 100644 index 0000000000000..8989c5fa46934 --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transparentizePalettes } from './utils'; +import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; + +const paletteServiceMock = chartPluginMock.createPaletteRegistry(); + +describe('transparentizePalettes', () => { + it('converts all colors to half-transparent', () => { + const newPalettes = transparentizePalettes(paletteServiceMock); + + const singlePalette = newPalettes.get('mocked'); + expect(singlePalette.getCategoricalColors(2)).toEqual(['#0000FF80', '#FFFF0080']); + expect( + singlePalette.getCategoricalColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ]) + ).toEqual('#0000FF80'); + + const firstPalette = newPalettes.getAll()[0]; + expect(firstPalette.getCategoricalColors(2)).toEqual(['#FF000080', '#00000080']); + expect( + firstPalette.getCategoricalColor([ + { + name: 'abc', + rankAtDepth: 0, + totalSeriesAtDepth: 5, + }, + ]) + ).toEqual('#00000080'); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/utils.ts b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts new file mode 100644 index 0000000000000..ec6e52b01864b --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/utils.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { scaleLinear } from 'd3-scale'; +import { + ChartColorConfiguration, + PaletteDefinition, + PaletteRegistry, + SeriesLayer, +} from 'src/plugins/charts/public'; +import { DatatableRow } from 'src/plugins/expressions'; +import Color from 'color'; +import type { GaugeVisualizationState } from '../../../common/expressions/gauge_chart'; + +type GaugeAccessors = 'maxAccessor' | 'minAccessor' | 'goalAccessor' | 'metricAccessor'; + +type GaugeAccessorsType = Pick; + +export const getValueFromAccessor = ( + accessorName: GaugeAccessors, + row?: DatatableRow, + state?: GaugeAccessorsType +) => { + if (row && state) { + const accessor = state[accessorName]; + const value = accessor && row[accessor]; + if (typeof value === 'number') { + return value; + } + if (value?.length) { + if (typeof value[value.length - 1] === 'number') { + return value[value.length - 1]; + } + } + } +}; + +export const getMaxValue = (row?: DatatableRow, state?: GaugeAccessorsType): number => { + const FALLBACK_VALUE = 100; + const currentValue = getValueFromAccessor('maxAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + if (row && state) { + const { metricAccessor, goalAccessor } = state; + const metricValue = metricAccessor && row[metricAccessor]; + const goalValue = goalAccessor && row[goalAccessor]; + const minValue = getMinValue(row, state); + if (metricValue != null) { + const numberValues = [minValue, goalValue, metricValue].filter((v) => typeof v === 'number'); + const biggerValue = Math.max(...numberValues); + const nicelyRounded = scaleLinear().domain([minValue, biggerValue]).nice().ticks(4); + if (nicelyRounded.length > 2) { + const ticksDifference = Math.abs(nicelyRounded[0] - nicelyRounded[1]); + return nicelyRounded[nicelyRounded.length - 1] + ticksDifference; + } + return minValue === biggerValue ? biggerValue + 1 : biggerValue; + } + } + return FALLBACK_VALUE; +}; + +export const getMinValue = (row?: DatatableRow, state?: GaugeAccessorsType) => { + const currentValue = getValueFromAccessor('minAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + const FALLBACK_VALUE = 0; + if (row && state) { + const { metricAccessor, maxAccessor } = state; + const metricValue = metricAccessor && row[metricAccessor]; + const maxValue = maxAccessor && row[maxAccessor]; + const numberValues = [metricValue, maxValue].filter((v) => typeof v === 'number'); + if (Math.min(...numberValues) <= 0) { + return Math.min(...numberValues) - 10; // TODO: TO THINK THROUGH + } + } + return FALLBACK_VALUE; +}; + +export const getGoalValue = (row?: DatatableRow, state?: GaugeVisualizationState) => { + const currentValue = getValueFromAccessor('goalAccessor', row, state); + if (currentValue != null) { + return currentValue; + } + const minValue = getMinValue(row, state); + const maxValue = getMaxValue(row, state); + return Math.round((maxValue - minValue) * 0.75 + minValue); +}; + +export const transparentizePalettes = (palettes: PaletteRegistry) => { + const addAlpha = (c: string | null) => (c ? new Color(c).hex() + `80` : `000000`); + const transparentizePalette = (palette: PaletteDefinition) => ({ + ...palette, + getCategoricalColor: ( + series: SeriesLayer[], + chartConfiguration?: ChartColorConfiguration, + state?: unknown + ) => addAlpha(palette.getCategoricalColor(series, chartConfiguration, state)), + getCategoricalColors: (size: number, state?: unknown): string[] => + palette.getCategoricalColors(size, state).map(addAlpha), + }); + + return { + ...palettes, + get: (name: string) => transparentizePalette(palettes.get(name)), + getAll: () => palettes.getAll().map((singlePalette) => transparentizePalette(singlePalette)), + }; +}; diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts new file mode 100644 index 0000000000000..e5e7f092db9ce --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.test.ts @@ -0,0 +1,590 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getGaugeVisualization, isNumericDynamicMetric, isNumericMetric } from './visualization'; +import { createMockDatasource, createMockFramePublicAPI } from '../../mocks'; +import { GROUP_ID } from './constants'; +import type { DatasourcePublicAPI, Operation } from '../../types'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { CustomPaletteParams, layerTypes } from '../../../common'; +import { + EXPRESSION_GAUGE_NAME, + GaugeVisualizationState, +} from '../../../common/expressions/gauge_chart'; +import { PaletteOutput } from 'src/plugins/charts/common'; + +function exampleState(): GaugeVisualizationState { + return { + layerId: 'test-layer', + layerType: layerTypes.DATA, + labelMajorMode: 'auto', + ticksPosition: 'auto', + shape: 'horizontalBullet', + }; +} + +const paletteService = chartPluginMock.createPaletteRegistry(); + +describe('gauge', () => { + let frame: ReturnType; + + beforeEach(() => { + frame = createMockFramePublicAPI(); + }); + + describe('#intialize', () => { + test('returns a default state', () => { + expect(getGaugeVisualization({ paletteService }).initialize(() => 'l1')).toEqual({ + layerId: 'l1', + layerType: layerTypes.DATA, + title: 'Empty Gauge chart', + shape: 'horizontalBullet', + labelMajorMode: 'auto', + ticksPosition: 'auto', + }); + }); + + test('returns persisted state', () => { + expect( + getGaugeVisualization({ paletteService }).initialize(() => 'test-layer', exampleState()) + ).toEqual(exampleState()); + }); + }); + + describe('#getConfiguration', () => { + beforeEach(() => { + const mockDatasource = createMockDatasource('testDatasource'); + + mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + label: 'MyOperation', + } as Operation); + + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + + afterEach(() => { + // some tests manipulate it, so restore a pristine version + frame = createMockFramePublicAPI(); + }); + + test('resolves configuration from complete state and available data', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + goalAccessor: 'goal-accessor', + }; + frame.activeData = { + first: { type: 'datatable', columns: [], rows: [{ 'metric-accessor': 200 }] }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getConfiguration({ state, frame, layerId: 'first' }) + ).toEqual({ + groups: [ + { + layerId: 'first', + groupId: GROUP_ID.METRIC, + groupLabel: 'Metric', + accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + filterOperations: isNumericDynamicMetric, + supportsMoreColumns: false, + required: true, + dataTestSubj: 'lnsGauge_metricDimensionPanel', + enableDimensionEditor: true, + supportFieldFormat: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MIN, + groupLabel: 'Minimum value', + accessors: [{ columnId: 'min-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: expect.any(Function), + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MAX, + groupLabel: 'Maximum value', + accessors: [{ columnId: 'max-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: expect.any(Function), + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.GOAL, + groupLabel: 'Goal value', + accessors: [{ columnId: 'goal-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + supportFieldFormat: false, + supportStaticValue: true, + }, + ], + }); + }); + + test('resolves configuration from partial state', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + minAccessor: 'min-accessor', + }; + expect( + getGaugeVisualization({ + paletteService, + }).getConfiguration({ state, frame, layerId: 'first' }) + ).toEqual({ + groups: [ + { + layerId: 'first', + groupId: GROUP_ID.METRIC, + groupLabel: 'Metric', + accessors: [], + filterOperations: isNumericDynamicMetric, + supportsMoreColumns: true, + required: true, + dataTestSubj: 'lnsGauge_metricDimensionPanel', + enableDimensionEditor: true, + supportFieldFormat: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MIN, + groupLabel: 'Minimum value', + accessors: [{ columnId: 'min-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: expect.any(Function), + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MAX, + groupLabel: 'Maximum value', + accessors: [], + filterOperations: isNumericMetric, + supportsMoreColumns: true, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: expect.any(Function), + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.GOAL, + groupLabel: 'Goal value', + accessors: [], + filterOperations: isNumericMetric, + supportsMoreColumns: true, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + supportFieldFormat: false, + supportStaticValue: true, + }, + ], + }); + }); + + test("resolves configuration when there's no access to active data in frame", () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + goalAccessor: 'goal-accessor', + }; + + frame.activeData = undefined; + + expect( + getGaugeVisualization({ + paletteService, + }).getConfiguration({ state, frame, layerId: 'first' }) + ).toEqual({ + groups: [ + { + layerId: 'first', + groupId: GROUP_ID.METRIC, + groupLabel: 'Metric', + accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }], + filterOperations: isNumericDynamicMetric, + supportsMoreColumns: false, + required: true, + dataTestSubj: 'lnsGauge_metricDimensionPanel', + enableDimensionEditor: true, + supportFieldFormat: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MIN, + groupLabel: 'Minimum value', + accessors: [{ columnId: 'min-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: expect.any(Function), + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.MAX, + groupLabel: 'Maximum value', + accessors: [{ columnId: 'max-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: expect.any(Function), + supportFieldFormat: false, + supportStaticValue: true, + }, + { + layerId: 'first', + groupId: GROUP_ID.GOAL, + groupLabel: 'Goal value', + accessors: [{ columnId: 'goal-accessor' }], + filterOperations: isNumericMetric, + supportsMoreColumns: false, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + supportFieldFormat: false, + supportStaticValue: true, + }, + ], + }); + }); + }); + + describe('#setDimension', () => { + test('set dimension correctly', () => { + const prevState: GaugeVisualizationState = { + ...exampleState(), + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + }; + expect( + getGaugeVisualization({ + paletteService, + }).setDimension({ + prevState, + layerId: 'first', + columnId: 'new-min-accessor', + groupId: 'min', + frame, + }) + ).toEqual({ + ...prevState, + minAccessor: 'new-min-accessor', + }); + }); + }); + + describe('#removeDimension', () => { + const prevState: GaugeVisualizationState = { + ...exampleState(), + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + palette: [] as unknown as PaletteOutput, + colorMode: 'palette', + ticksPosition: 'bands', + }; + test('removes metricAccessor correctly', () => { + expect( + getGaugeVisualization({ + paletteService, + }).removeDimension({ + prevState, + layerId: 'first', + columnId: 'metric-accessor', + frame, + }) + ).toEqual({ + ...exampleState(), + minAccessor: 'min-accessor', + }); + }); + test('removes minAccessor correctly', () => { + expect( + getGaugeVisualization({ + paletteService, + }).removeDimension({ + prevState, + layerId: 'first', + columnId: 'min-accessor', + frame, + }) + ).toEqual({ + ...exampleState(), + metricAccessor: 'metric-accessor', + palette: [] as unknown as PaletteOutput, + colorMode: 'palette', + ticksPosition: 'bands', + }); + }); + }); + describe('#getSupportedLayers', () => { + it('should return a single layer type', () => { + expect( + getGaugeVisualization({ + paletteService, + }).getSupportedLayers() + ).toHaveLength(1); + }); + }); + describe('#getLayerType', () => { + it('should return the type only if the layer is in the state', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + minAccessor: 'minAccessor', + goalAccessor: 'value-accessor', + }; + const instance = getGaugeVisualization({ + paletteService, + }); + expect(instance.getLayerType('test-layer', state)).toEqual(layerTypes.DATA); + expect(instance.getLayerType('foo', state)).toBeUndefined(); + }); + }); + + describe('#toExpression', () => { + let datasourceLayers: Record; + beforeEach(() => { + const mockDatasource = createMockDatasource('testDatasource'); + mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + label: 'MyOperation', + } as Operation); + datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + test('creates an expression based on state and attributes', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + minAccessor: 'min-accessor', + goalAccessor: 'goal-accessor', + metricAccessor: 'metric-accessor', + maxAccessor: 'max-accessor', + labelMinor: 'Subtitle', + }; + const attributes = { + title: 'Test', + }; + expect( + getGaugeVisualization({ + paletteService, + }).toExpression(state, datasourceLayers, attributes) + ).toEqual({ + type: 'expression', + chain: [ + { + type: 'function', + function: EXPRESSION_GAUGE_NAME, + arguments: { + title: ['Test'], + description: [''], + metricAccessor: ['metric-accessor'], + minAccessor: ['min-accessor'], + maxAccessor: ['max-accessor'], + goalAccessor: ['goal-accessor'], + colorMode: ['none'], + ticksPosition: ['auto'], + labelMajorMode: ['auto'], + labelMinor: ['Subtitle'], + labelMajor: [], + palette: [], + shape: ['horizontalBullet'], + }, + }, + ], + }); + }); + test('returns null with a missing metric accessor', () => { + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + minAccessor: 'minAccessor', + }; + const attributes = { + title: 'Test', + }; + expect( + getGaugeVisualization({ + paletteService, + }).toExpression(state, datasourceLayers, attributes) + ).toEqual(null); + }); + }); + + describe('#getErrorMessages', () => { + it('returns undefined if no error is raised', () => { + const error = getGaugeVisualization({ + paletteService, + }).getErrorMessages(exampleState()); + expect(error).not.toBeDefined(); + }); + }); + + describe('#getWarningMessages', () => { + beforeEach(() => { + const mockDatasource = createMockDatasource('testDatasource'); + mockDatasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + label: 'MyOperation', + } as Operation); + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + }); + const state: GaugeVisualizationState = { + ...exampleState(), + layerId: 'first', + metricAccessor: 'metric-accessor', + minAccessor: 'min-accessor', + maxAccessor: 'max-accessor', + goalAccessor: 'goal-accessor', + }; + it('should not warn for data in bounds', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'min-accessor': 0, + 'metric-accessor': 5, + 'max-accessor': 10, + 'goal-accessor': 8, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(0); + }); + it('should warn when minimum value is greater than metric value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': -1, + 'min-accessor': 1, + 'max-accessor': 3, + 'goal-accessor': 2, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + + it('should warn when metric value is greater than maximum value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': 10, + 'min-accessor': -10, + 'max-accessor': 0, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + it('should warn when goal value is greater than maximum value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': 5, + 'min-accessor': 0, + 'max-accessor': 10, + 'goal-accessor': 15, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + it('should warn when minimum value is greater than goal value', () => { + frame.activeData = { + first: { + type: 'datatable', + columns: [], + rows: [ + { + 'metric-accessor': 5, + 'min-accessor': 0, + 'max-accessor': 10, + 'goal-accessor': -5, + }, + ], + }, + }; + + expect( + getGaugeVisualization({ + paletteService, + }).getWarningMessages!(state, frame) + ).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx new file mode 100644 index 0000000000000..59dd2ac161a8a --- /dev/null +++ b/x-pack/plugins/lens/public/visualizations/gauge/visualization.tsx @@ -0,0 +1,454 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n-react'; +import { Ast } from '@kbn/interpreter/common'; +import { PaletteRegistry } from '../../../../../../src/plugins/charts/public'; +import type { DatasourcePublicAPI, OperationMetadata, Visualization } from '../../types'; +import { getSuggestions } from './suggestions'; +import { GROUP_ID, LENS_GAUGE_ID } from './constants'; +import { GaugeToolbar } from './toolbar_component'; +import { LensIconChartGaugeHorizontal, LensIconChartGaugeVertical } from '../../assets/chart_gauge'; +import { applyPaletteParams, CUSTOM_PALETTE, getStopsForFixedMode } from '../../shared_components'; +import { GaugeDimensionEditor } from './dimension_editor'; +import { CustomPaletteParams, layerTypes } from '../../../common'; +import { generateId } from '../../id_generator'; +import { getGoalValue, getMaxValue, getMinValue } from './utils'; + +import { + GaugeShapes, + GaugeArguments, + EXPRESSION_GAUGE_NAME, + GaugeVisualizationState, +} from '../../../common/expressions/gauge_chart'; + +const groupLabelForGauge = i18n.translate('xpack.lens.metric.groupLabel', { + defaultMessage: 'Goal and single value', +}); + +interface GaugeVisualizationDeps { + paletteService: PaletteRegistry; +} + +export const isNumericMetric = (op: OperationMetadata) => + !op.isBucketed && op.dataType === 'number'; + +export const isNumericDynamicMetric = (op: OperationMetadata) => + isNumericMetric(op) && !op.isStaticValue; + +export const CHART_NAMES = { + horizontalBullet: { + icon: LensIconChartGaugeHorizontal, + label: i18n.translate('xpack.lens.gaugeHorizontal.gaugeLabel', { + defaultMessage: 'Gauge horizontal', + }), + groupLabel: groupLabelForGauge, + }, + verticalBullet: { + icon: LensIconChartGaugeVertical, + label: i18n.translate('xpack.lens.gaugeVertical.gaugeLabel', { + defaultMessage: 'Gauge vertical', + }), + groupLabel: groupLabelForGauge, + }, +}; + +function computePaletteParams(params: CustomPaletteParams) { + return { + ...params, + // rewrite colors and stops as two distinct arguments + colors: (params?.stops || []).map(({ color }) => color), + stops: params?.name === 'custom' ? (params?.stops || []).map(({ stop }) => stop) : [], + reverse: false, // managed at UI level + }; +} + +const toExpression = ( + paletteService: PaletteRegistry, + state: GaugeVisualizationState, + datasourceLayers: Record, + attributes?: Partial> +): Ast | null => { + const datasource = datasourceLayers[state.layerId]; + + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + if (!originalOrder || !state.metricAccessor) { + return null; + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: EXPRESSION_GAUGE_NAME, + arguments: { + title: [attributes?.title ?? ''], + description: [attributes?.description ?? ''], + metricAccessor: [state.metricAccessor ?? ''], + minAccessor: [state.minAccessor ?? ''], + maxAccessor: [state.maxAccessor ?? ''], + goalAccessor: [state.goalAccessor ?? ''], + shape: [state.shape ?? GaugeShapes.horizontalBullet], + colorMode: [state?.colorMode ?? 'none'], + palette: state.palette?.params + ? [ + paletteService + .get(CUSTOM_PALETTE) + .toExpression( + computePaletteParams((state.palette?.params || {}) as CustomPaletteParams) + ), + ] + : [], + ticksPosition: state.ticksPosition ? [state.ticksPosition] : ['auto'], + labelMinor: state.labelMinor ? [state.labelMinor] : [], + labelMajor: state.labelMajor ? [state.labelMajor] : [], + labelMajorMode: state.labelMajorMode ? [state.labelMajorMode] : ['auto'], + }, + }, + ], + }; +}; + +export const getGaugeVisualization = ({ + paletteService, +}: GaugeVisualizationDeps): Visualization => ({ + id: LENS_GAUGE_ID, + + visualizationTypes: [ + { + ...CHART_NAMES.horizontalBullet, + id: GaugeShapes.horizontalBullet, + showExperimentalBadge: true, + }, + { + ...CHART_NAMES.verticalBullet, + id: GaugeShapes.verticalBullet, + showExperimentalBadge: true, + }, + ], + getVisualizationTypeId(state) { + return state.shape; + }, + getLayerIds(state) { + return [state.layerId]; + }, + clearLayer(state) { + const newState = { ...state }; + delete newState.metricAccessor; + delete newState.minAccessor; + delete newState.maxAccessor; + delete newState.goalAccessor; + delete newState.palette; + delete newState.colorMode; + return newState; + }, + + getDescription(state) { + if (state.shape === GaugeShapes.horizontalBullet) { + return CHART_NAMES.horizontalBullet; + } + return CHART_NAMES.verticalBullet; + }, + + switchVisualizationType: (visualizationTypeId, state) => { + return { + ...state, + shape: + visualizationTypeId === GaugeShapes.horizontalBullet + ? GaugeShapes.horizontalBullet + : GaugeShapes.verticalBullet, + }; + }, + + initialize(addNewLayer, state, mainPalette) { + return ( + state || { + layerId: addNewLayer(), + layerType: layerTypes.DATA, + title: 'Empty Gauge chart', + shape: GaugeShapes.horizontalBullet, + palette: mainPalette, + ticksPosition: 'auto', + labelMajorMode: 'auto', + } + ); + }, + getSuggestions, + + getConfiguration({ state, frame }) { + const hasColoring = Boolean(state.colorMode !== 'none' && state.palette?.params?.stops); + + const row = state?.layerId ? frame?.activeData?.[state?.layerId]?.rows?.[0] : undefined; + let palette; + if (!(row == null || state?.metricAccessor == null || state?.palette == null || !hasColoring)) { + const currentMinMax = { min: getMinValue(row, state), max: getMaxValue(row, state) }; + + const displayStops = applyPaletteParams(paletteService, state?.palette, currentMinMax); + palette = getStopsForFixedMode(displayStops, state?.palette?.params?.colorStops); + } + + return { + groups: [ + { + supportFieldFormat: true, + layerId: state.layerId, + groupId: GROUP_ID.METRIC, + groupLabel: i18n.translate('xpack.lens.gauge.metricLabel', { + defaultMessage: 'Metric', + }), + accessors: state.metricAccessor + ? [ + palette + ? { + columnId: state.metricAccessor, + triggerIcon: 'colorBy', + palette, + } + : { + columnId: state.metricAccessor, + triggerIcon: 'none', + }, + ] + : [], + filterOperations: isNumericDynamicMetric, + supportsMoreColumns: !state.metricAccessor, + required: true, + dataTestSubj: 'lnsGauge_metricDimensionPanel', + enableDimensionEditor: true, + }, + { + supportStaticValue: true, + supportFieldFormat: false, + layerId: state.layerId, + groupId: GROUP_ID.MIN, + groupLabel: i18n.translate('xpack.lens.gauge.minValueLabel', { + defaultMessage: 'Minimum value', + }), + accessors: state.minAccessor ? [{ columnId: state.minAccessor }] : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.minAccessor, + dataTestSubj: 'lnsGauge_minDimensionPanel', + prioritizedOperation: 'min', + suggestedValue: () => (state.metricAccessor ? getMinValue(row, state) : undefined), + }, + { + supportStaticValue: true, + supportFieldFormat: false, + layerId: state.layerId, + groupId: GROUP_ID.MAX, + groupLabel: i18n.translate('xpack.lens.gauge.maxValueLabel', { + defaultMessage: 'Maximum value', + }), + accessors: state.maxAccessor ? [{ columnId: state.maxAccessor }] : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.maxAccessor, + dataTestSubj: 'lnsGauge_maxDimensionPanel', + prioritizedOperation: 'max', + suggestedValue: () => (state.metricAccessor ? getMaxValue(row, state) : undefined), + }, + { + supportStaticValue: true, + supportFieldFormat: false, + layerId: state.layerId, + groupId: GROUP_ID.GOAL, + groupLabel: i18n.translate('xpack.lens.gauge.goalValueLabel', { + defaultMessage: 'Goal value', + }), + accessors: state.goalAccessor ? [{ columnId: state.goalAccessor }] : [], + filterOperations: isNumericMetric, + supportsMoreColumns: !state.goalAccessor, + required: false, + dataTestSubj: 'lnsGauge_goalDimensionPanel', + }, + ], + }; + }, + + setDimension({ prevState, layerId, columnId, groupId, previousColumn }) { + const update: Partial = {}; + if (groupId === GROUP_ID.MIN) { + update.minAccessor = columnId; + } + if (groupId === GROUP_ID.MAX) { + update.maxAccessor = columnId; + } + if (groupId === GROUP_ID.GOAL) { + update.goalAccessor = columnId; + } + if (groupId === GROUP_ID.METRIC) { + update.metricAccessor = columnId; + } + return { + ...prevState, + ...update, + }; + }, + + removeDimension({ prevState, layerId, columnId }) { + const update = { ...prevState }; + + if (prevState.goalAccessor === columnId) { + delete update.goalAccessor; + } + if (prevState.minAccessor === columnId) { + delete update.minAccessor; + } + if (prevState.maxAccessor === columnId) { + delete update.maxAccessor; + } + if (prevState.metricAccessor === columnId) { + delete update.metricAccessor; + delete update.palette; + delete update.colorMode; + update.ticksPosition = 'auto'; + } + + return update; + }, + + renderDimensionEditor(domElement, props) { + render( + + + , + domElement + ); + }, + + renderToolbar(domElement, props) { + render( + + + , + domElement + ); + }, + + getSupportedLayers(state, frame) { + const row = state?.layerId ? frame?.activeData?.[state?.layerId]?.rows?.[0] : undefined; + + const minAccessorValue = getMinValue(row, state); + const maxAccessorValue = getMaxValue(row, state); + const goalAccessorValue = getGoalValue(row, state); + + return [ + { + type: layerTypes.DATA, + label: i18n.translate('xpack.lens.gauge.addLayer', { + defaultMessage: 'Add visualization layer', + }), + initialDimensions: state + ? [ + { + groupId: 'min', + columnId: generateId(), + dataType: 'number', + label: 'minAccessor', + staticValue: minAccessorValue, + }, + { + groupId: 'max', + columnId: generateId(), + dataType: 'number', + label: 'maxAccessor', + staticValue: maxAccessorValue, + }, + { + groupId: 'goal', + columnId: generateId(), + dataType: 'number', + label: 'goalAccessor', + staticValue: goalAccessorValue, + }, + ] + : undefined, + }, + ]; + }, + + getLayerType(layerId, state) { + if (state?.layerId === layerId) { + return state.layerType; + } + }, + + toExpression: (state, datasourceLayers, attributes) => + toExpression(paletteService, state, datasourceLayers, { ...attributes }), + toPreviewExpression: (state, datasourceLayers) => + toExpression(paletteService, state, datasourceLayers), + + getErrorMessages(state) { + // not possible to break it? + return undefined; + }, + + getWarningMessages(state, frame) { + const { maxAccessor, minAccessor, goalAccessor, metricAccessor } = state; + if (!maxAccessor && !minAccessor && !goalAccessor && !metricAccessor) { + // nothing configured yet + return; + } + if (!metricAccessor) { + return []; + } + + const row = frame?.activeData?.[state.layerId]?.rows?.[0]; + if (!row) { + return []; + } + const metricValue = row[metricAccessor]; + const maxValue = maxAccessor && row[maxAccessor]; + const minValue = minAccessor && row[minAccessor]; + const goalValue = goalAccessor && row[goalAccessor]; + + const warnings = []; + if (typeof minValue === 'number') { + if (minValue > metricValue) { + warnings.push([ + , + ]); + } + if (minValue > goalValue) { + warnings.push([ + , + ]); + } + } + + if (typeof maxValue === 'number') { + if (metricValue > maxValue) { + warnings.push([ + , + ]); + } + + if (typeof goalValue === 'number' && goalValue > maxValue) { + warnings.push([ + , + ]); + } + } + + return warnings; + }, +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index bb3b5bfcbfec6..65425b04129d3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -45,7 +45,7 @@ import { shallow } from 'enzyme'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { mountWithIntl } from '@kbn/test/jest'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { EmptyPlaceholder } from '../shared_components/empty_placeholder'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { XyEndzones } from './x_domain'; const onClickValue = jest.fn(); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index d22a8034cdf2b..01359c68c6da3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -44,6 +44,7 @@ import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RenderMode } from 'src/plugins/expressions'; import { ThemeServiceStart } from 'kibana/public'; +import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public'; import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types'; import type { LensMultiTable, FormatFactory } from '../../common'; @@ -61,7 +62,6 @@ import { useActiveCursor, } from '../../../../../src/plugins/charts/public'; import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../src/plugins/charts/common'; -import { EmptyPlaceholder } from '../shared_components'; import { getFitOptions } from './fitting_functions'; import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration'; import { getColorAssignments } from './color_assignment'; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index d536a18b6ab79..8957a522303e0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -46,7 +46,9 @@ import { groupAxesByType } from './axes_configuration'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; + const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; +const isNumericDynamicMetric = (op: OperationMetadata) => isNumericMetric(op) && !op.isStaticValue; const isBucketed = (op: OperationMetadata) => op.isBucketed; function getVisualizationType(state: State): VisualizationType | 'mixed' { @@ -438,7 +440,7 @@ export const getXyVisualization = ({ groupId: 'y', groupLabel: getAxisName('y', { isHorizontal }), accessors: mappedAccessors, - filterOperations: isNumericMetric, + filterOperations: isNumericDynamicMetric, supportsMoreColumns: true, required: true, dataTestSubj: 'lnsXY_yDimensionPanel', @@ -704,8 +706,7 @@ export const getXyVisualization = ({ {label}, }} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index d7b48553ce73a..89714dff04a62 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -38,6 +38,18 @@ describe('xy_suggestions', () => { }; } + function staticValueCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + label: `Static value: ${columnId}`, + isBucketed: false, + isStaticValue: true, + }, + }; + } + function strCol(columnId: string): TableSuggestionColumn { return { columnId, @@ -120,6 +132,21 @@ describe('xy_suggestions', () => { ); }); + test('rejects the configuration when metric isStaticValue', () => { + (generateId as jest.Mock).mockReturnValueOnce('aaa'); + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [staticValueCol('value'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + keptLayerIds: [], + }); + + expect(suggestions).toHaveLength(0); + }); + test('rejects incomplete configurations if there is a state already but no sub visualization id', () => { expect( ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 2e275c455a4d0..0fa07f4f9ebb6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -69,7 +69,10 @@ export function getSuggestions({ }); } - if (incompleteTable && state && !subVisualizationId) { + if ( + (incompleteTable && state && !subVisualizationId) || + table.columns.some((col) => col.operation.isStaticValue) + ) { // reject incomplete configurations if the sub visualization isn't specifically requested // this allows to switch chart types via switcher with incomplete configurations, but won't // cause incomplete suggestions getting auto applied on dropped fields diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 83379fe48ac9e..43c8135127871 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -177,10 +177,7 @@ export class LicensingPlugin implements Plugin): Promise => { const client = isPromise(clusterClient) ? await clusterClient : clusterClient; try { - const { body: response } = await client.asInternalUser.xpack.info({ - // @ts-expect-error `accept_enterprise` is not present in the client definition - accept_enterprise: true, - }); + const { body: response } = await client.asInternalUser.xpack.info(); const normalizedLicense = response.license && response.license.type !== 'missing' ? normalizeServerLicense(response.license) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 43cca5f0c6a07..782967aca8cf3 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -36,8 +36,6 @@ export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile'; // Centroids are a single point for representing lines, multiLines, polygons, and multiPolygons export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; -export const MVT_TOKEN_PARAM_NAME = 'token'; - export function getNewMapPath() { return `/${MAPS_APP_PATH}/${MAP_PATH}`; } diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts index 7eff8943f42c1..dd3ad847641ed 100644 --- a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts @@ -8,11 +8,11 @@ import { AbstractField, IField } from './field'; import { FIELD_ORIGIN, MVT_FIELD_TYPE } from '../../../common/constants'; import { IVectorSource } from '../sources/vector_source'; -import { ITiledSingleLayerVectorSource } from '../sources/tiled_single_layer_vector_source'; +import { IMvtVectorSource } from '../sources/vector_source'; import { MVTFieldDescriptor } from '../../../common/descriptor_types'; export class MVTField extends AbstractField implements IField { - private readonly _source: ITiledSingleLayerVectorSource; + private readonly _source: IMvtVectorSource; private readonly _type: MVT_FIELD_TYPE; constructor({ fieldName, @@ -21,7 +21,7 @@ export class MVTField extends AbstractField implements IField { origin, }: { fieldName: string; - source: ITiledSingleLayerVectorSource; + source: IMvtVectorSource; origin: FIELD_ORIGIN; type: MVT_FIELD_TYPE; }) { diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx index 8f7471f255a5d..c6d594617c448 100644 --- a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx @@ -7,15 +7,13 @@ import type { Map as MbMap, Layer as MbLayer, Style as MbStyle } from '@kbn/mapbox-gl'; import _ from 'lodash'; +// @ts-expect-error +import { RGBAImage } from './image_utils'; import { AbstractLayer } from '../layer'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { DataRequest } from '../../util/data_request'; import { isRetina } from '../../../util'; -import { - addSpriteSheetToMapFromImageData, - loadSpriteSheetImageData, -} from '../../../connected_components/mb_map/utils'; import { DataRequestContext } from '../../../actions'; import { EMSTMSSource } from '../../sources/ems_tms_source'; import { TileStyle } from '../../styles/tile/tile_style'; @@ -118,7 +116,7 @@ export class EmsVectorTileLayer extends AbstractLayer { startLoading(SOURCE_DATA_REQUEST_ID, requestToken, nextMeta); const styleAndSprites = await this.getSource().getVectorStyleSheetAndSpriteMeta(isRetina()); const spriteSheetImageData = styleAndSprites.spriteMeta - ? await loadSpriteSheetImageData(styleAndSprites.spriteMeta.png) + ? await this._loadSpriteSheetImageData(styleAndSprites.spriteMeta.png) : undefined; const data = { ...styleAndSprites, @@ -210,6 +208,60 @@ export class EmsVectorTileLayer extends AbstractLayer { }); } + _getImageData(img: HTMLImageElement) { + const canvas = window.document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('failed to create canvas 2d context'); + } + canvas.width = img.width; + canvas.height = img.height; + context.drawImage(img, 0, 0, img.width, img.height); + return context.getImageData(0, 0, img.width, img.height); + } + + _isCrossOriginUrl(url: string) { + const a = window.document.createElement('a'); + a.href = url; + return ( + a.protocol !== window.document.location.protocol || + a.host !== window.document.location.host || + a.port !== window.document.location.port + ); + } + + _loadSpriteSheetImageData(imgUrl: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + if (this._isCrossOriginUrl(imgUrl)) { + image.crossOrigin = 'Anonymous'; + } + image.onload = (event) => { + resolve(this._getImageData(image)); + }; + image.onerror = (e) => { + reject(e); + }; + image.src = imgUrl; + }); + } + + _addSpriteSheetToMapFromImageData(json: EmsSpriteSheet, imgData: ImageData, mbMap: MbMap) { + for (const imageId in json) { + if (!(json.hasOwnProperty(imageId) && !mbMap.hasImage(imageId))) { + continue; + } + const { width, height, x, y, sdf, pixelRatio } = json[imageId]; + if (typeof width !== 'number' || typeof height !== 'number') { + continue; + } + + const data = new RGBAImage({ width, height }); + RGBAImage.copy(imgData, data, { x, y }, { x: 0, y: 0 }, { width, height }); + mbMap.addImage(imageId, data, { pixelRatio, sdf }); + } + } + syncLayerWithMB(mbMap: MbMap) { const vectorStyle = this._getVectorStyle(); if (!vectorStyle) { @@ -252,7 +304,7 @@ export class EmsVectorTileLayer extends AbstractLayer { if (!imageData) { return; } - addSpriteSheetToMapFromImageData(newJson, imageData, mbMap); + this._addSpriteSheetToMapFromImageData(newJson, imageData, mbMap); // sync layers const layers = vectorStyle.layers ? vectorStyle.layers : []; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/image_utils.js b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/image_utils.js similarity index 98% rename from x-pack/plugins/maps/public/connected_components/mb_map/image_utils.js rename to x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/image_utils.js index 3b19b474d699b..b907bea3cbad7 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/image_utils.js +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/image_utils.js @@ -8,7 +8,7 @@ /* @notice * This product includes code that is adapted from mapbox-gl-js, which is * available under a "BSD-3-Clause" license. - * https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/image.js + * https://github.com/mapbox/mapbox-gl-js/blob/v1.13.2/src/util/image.js * * Copyright (c) 2016, Mapbox * diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 9b49d2b597e46..a977ed10fdade 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -14,7 +14,7 @@ import { ESGeoGridSource } from '../../sources/es_geo_grid_source'; import { syncBoundsData, MvtSourceData, syncMvtSourceData } from '../vector_layer'; import { DataRequestContext } from '../../../actions'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; -import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; +import { IMvtVectorSource } from '../../sources/vector_source'; export class HeatmapLayer extends AbstractLayer { private readonly _style: HeatmapStyle; @@ -91,7 +91,7 @@ export class HeatmapLayer extends AbstractLayer { this.getQuery(), syncContext.isForceRefresh ), - source: this.getSource() as ITiledSingleLayerVectorSource, + source: this.getSource() as IMvtVectorSource, syncContext, }); } @@ -111,7 +111,7 @@ export class HeatmapLayer extends AbstractLayer { return false; } - return mbSource.tiles?.[0] !== sourceData.urlTemplate; + return mbSource.tiles?.[0] !== sourceData.tileUrl; } syncLayerWithMB(mbMap: MbMap) { @@ -130,9 +130,9 @@ export class HeatmapLayer extends AbstractLayer { if (!mbSource) { mbMap.addSource(mbSourceId, { type: 'vector', - tiles: [sourceData.urlTemplate], - minzoom: sourceData.minSourceZoom, - maxzoom: sourceData.maxSourceZoom, + tiles: [sourceData.tileUrl], + minzoom: sourceData.tileMinZoom, + maxzoom: sourceData.tileMaxZoom, }); } @@ -142,7 +142,7 @@ export class HeatmapLayer extends AbstractLayer { id: heatmapLayerId, type: 'heatmap', source: mbSourceId, - ['source-layer']: sourceData.layerName, + ['source-layer']: sourceData.tileSourceLayer, paint: {}, }); } @@ -153,7 +153,7 @@ export class HeatmapLayer extends AbstractLayer { } const metricField = metricFields[0]; - // do not use tile meta features from previous urlTemplate to avoid styling new tiles from previous tile meta features + // do not use tile meta features from previous tile URL to avoid styling new tiles from previous tile meta features const tileMetaFeatures = this._requiresPrevSourceCleanup(mbMap) ? [] : this._getMetaFromTiles(); let max = 0; for (let i = 0; i < tileMetaFeatures.length; i++) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts index 0862ae69376d8..e56884298a0b6 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.test.ts @@ -13,12 +13,12 @@ jest.mock('uuid/v4', () => { import sinon from 'sinon'; import { MockSyncContext } from '../../__fixtures__/mock_sync_context'; -import { ITiledSingleLayerVectorSource } from '../../../sources/tiled_single_layer_vector_source'; +import { IMvtVectorSource } from '../../../sources/vector_source'; import { DataRequest } from '../../../util/data_request'; import { syncMvtSourceData } from './mvt_source_data'; const mockSource = { - getLayerName: () => { + getTileSourceLayer: () => { return 'aggs'; }, getMinZoom: () => { @@ -27,14 +27,8 @@ const mockSource = { getMaxZoom: () => { return 14; }, - getUrlTemplateWithMeta: () => { - return { - refreshTokenParamName: 'token', - layerName: 'aggs', - minSourceZoom: 4, - maxSourceZoom: 14, - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', - }; + getTileUrl: () => { + return 'https://example.com/{x}/{y}/{z}.pbf'; }, isTimeAware: () => { return true; @@ -48,7 +42,7 @@ const mockSource = { isGeoGridPrecisionAware: () => { return false; }, -} as unknown as ITiledSingleLayerVectorSource; +} as unknown as IMvtVectorSource; describe('syncMvtSourceData', () => { test('Should sync source data when there are no previous data request', async () => { @@ -78,12 +72,11 @@ describe('syncMvtSourceData', () => { const call = syncContext.stopLoading.getCall(0); const sourceData = call.args[2]; expect(sourceData).toEqual({ - minSourceZoom: 4, - maxSourceZoom: 14, - layerName: 'aggs', - refreshTokenParamName: 'token', - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', - urlToken: '12345', + tileMinZoom: 4, + tileMaxZoom: 14, + tileSourceLayer: 'aggs', + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf', + refreshToken: '12345', }); }); @@ -107,12 +100,11 @@ describe('syncMvtSourceData', () => { }, getData: () => { return { - minSourceZoom: 4, - maxSourceZoom: 14, - layerName: 'aggs', - refreshTokenParamName: 'token', - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', - urlToken: '12345', + tileMinZoom: 4, + tileMaxZoom: 14, + tileSourceLayer: 'aggs', + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', + refreshToken: '12345', }; }, } as unknown as DataRequest, @@ -146,12 +138,11 @@ describe('syncMvtSourceData', () => { }, getData: () => { return { - minSourceZoom: 4, - maxSourceZoom: 14, - layerName: 'aggs', - refreshTokenParamName: 'token', - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', - urlToken: '12345', + tileMinZoom: 4, + tileMaxZoom: 14, + tileSourceLayer: 'aggs', + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', + refreshToken: '12345', }; }, } as unknown as DataRequest, @@ -173,7 +164,7 @@ describe('syncMvtSourceData', () => { sinon.assert.calledOnce(syncContext.stopLoading); }); - test('Should re-sync when layerName source state changes: ', async () => { + test('Should re-sync when tileSourceLayer source state changes: ', async () => { const syncContext = new MockSyncContext({ dataFilters: {} }); const prevRequestMeta = { ...syncContext.dataFilters, @@ -193,12 +184,11 @@ describe('syncMvtSourceData', () => { }, getData: () => { return { - minSourceZoom: 4, - maxSourceZoom: 14, - layerName: 'barfoo', // layerName is different then mockSource - refreshTokenParamName: 'token', - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', - urlToken: '12345', + tileMinZoom: 4, + tileMaxZoom: 14, + tileSourceLayer: 'barfoo', // tileSourceLayer is different then mockSource + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', + refreshToken: '12345', }; }, } as unknown as DataRequest, @@ -232,12 +222,11 @@ describe('syncMvtSourceData', () => { }, getData: () => { return { - minSourceZoom: 2, // minSourceZoom is different then mockSource - maxSourceZoom: 14, - layerName: 'aggs', - refreshTokenParamName: 'token', - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', - urlToken: '12345', + tileMinZoom: 2, // tileMinZoom is different then mockSource + tileMaxZoom: 14, + tileSourceLayer: 'aggs', + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', + refreshToken: '12345', }; }, } as unknown as DataRequest, @@ -271,12 +260,11 @@ describe('syncMvtSourceData', () => { }, getData: () => { return { - minSourceZoom: 4, - maxSourceZoom: 9, // minSourceZoom is different then mockSource - layerName: 'aggs', - refreshTokenParamName: 'token', - urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', - urlToken: '12345', + tileMinZoom: 4, + tileMaxZoom: 9, // tileMinZoom is different then mockSource + tileSourceLayer: 'aggs', + tileUrl: 'https://example.com/{x}/{y}/{z}.pbf?token=12345', + refreshToken: '12345', }; }, } as unknown as DataRequest, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts index f38907db30ee7..5d4008fc19220 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_source_data.ts @@ -6,22 +6,21 @@ */ import uuid from 'uuid/v4'; -import { parse as parseUrl } from 'url'; import { SOURCE_DATA_REQUEST_ID } from '../../../../../common/constants'; import { Timeslice, VectorSourceRequestMeta } from '../../../../../common/descriptor_types'; import { DataRequest } from '../../../util/data_request'; import { DataRequestContext } from '../../../../actions'; import { canSkipSourceUpdate } from '../../../util/can_skip_fetch'; -import { - ITiledSingleLayerMvtParams, - ITiledSingleLayerVectorSource, -} from '../../../sources/tiled_single_layer_vector_source'; +import { IMvtVectorSource } from '../../../sources/vector_source'; // shape of sourceDataRequest.getData() -export type MvtSourceData = ITiledSingleLayerMvtParams & { - urlTemplate: string; - urlToken: string; -}; +export interface MvtSourceData { + tileSourceLayer: string; + tileMinZoom: number; + tileMaxZoom: number; + tileUrl: string; + refreshToken: string; +} export async function syncMvtSourceData({ layerId, @@ -33,7 +32,7 @@ export async function syncMvtSourceData({ layerId: string; prevDataRequest: DataRequest | undefined; requestMeta: VectorSourceRequestMeta; - source: ITiledSingleLayerVectorSource; + source: IMvtVectorSource; syncContext: DataRequestContext; }): Promise { const requestToken: symbol = Symbol(`${layerId}-${SOURCE_DATA_REQUEST_ID}`); @@ -42,9 +41,9 @@ export async function syncMvtSourceData({ if (prevData) { const noChangesInSourceState: boolean = - prevData.layerName === source.getLayerName() && - prevData.minSourceZoom === source.getMinZoom() && - prevData.maxSourceZoom === source.getMaxZoom(); + prevData.tileSourceLayer === source.getTileSourceLayer() && + prevData.tileMinZoom === source.getMinZoom() && + prevData.tileMaxZoom === source.getMaxZoom(); const noChangesInSearchState: boolean = await canSkipSourceUpdate({ extentAware: false, // spatial extent knowledge is already fully automated by tile-loading based on pan-zooming source, @@ -63,26 +62,18 @@ export async function syncMvtSourceData({ syncContext.startLoading(SOURCE_DATA_REQUEST_ID, requestToken, requestMeta); try { - const urlToken = + const refreshToken = !prevData || (requestMeta.isForceRefresh && requestMeta.applyForceRefresh) ? uuid() - : prevData.urlToken; - - const newUrlTemplateAndMeta = await source.getUrlTemplateWithMeta(requestMeta); - - let urlTemplate; - if (newUrlTemplateAndMeta.refreshTokenParamName) { - const parsedUrl = parseUrl(newUrlTemplateAndMeta.urlTemplate, true); - const separator = !parsedUrl.query || Object.keys(parsedUrl.query).length === 0 ? '?' : '&'; - urlTemplate = `${newUrlTemplateAndMeta.urlTemplate}${separator}${newUrlTemplateAndMeta.refreshTokenParamName}=${urlToken}`; - } else { - urlTemplate = newUrlTemplateAndMeta.urlTemplate; - } + : prevData.refreshToken; + const tileUrl = await source.getTileUrl(requestMeta, refreshToken); const sourceData = { - ...newUrlTemplateAndMeta, - urlToken, - urlTemplate, + tileUrl, + tileSourceLayer: source.getTileSourceLayer(), + tileMinZoom: source.getMinZoom(), + tileMaxZoom: source.getMaxZoom(), + refreshToken, }; syncContext.stopLoading(SOURCE_DATA_REQUEST_ID, requestToken, sourceData, {}); } catch (error) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index a6c78f786f7e2..4e4e76d3634f4 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -34,8 +34,7 @@ const defaultConfig = { function createLayer( layerOptions: Partial = {}, sourceOptions: Partial = {}, - isTimeAware: boolean = false, - includeToken: boolean = false + isTimeAware: boolean = false ): MvtVectorLayer { const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = { type: SOURCE_TYPES.MVT_SINGLE_LAYER, @@ -54,19 +53,6 @@ function createLayer( }; } - if (includeToken) { - mvtSource.getUrlTemplateWithMeta = async (...args) => { - const superReturn = await MVTSingleLayerVectorSource.prototype.getUrlTemplateWithMeta.call( - mvtSource, - ...args - ); - return { - ...superReturn, - refreshTokenParamName: 'token', - }; - }; - } - const defaultLayerOptions = { ...layerOptions, sourceDescriptor, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index 5ac95c9a91f64..a52ba99942c58 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -20,7 +20,7 @@ import { AbstractVectorLayer, VectorLayerArguments, } from '../vector_layer'; -import { ITiledSingleLayerVectorSource } from '../../../sources/tiled_single_layer_vector_source'; +import { IMvtVectorSource } from '../../../sources/vector_source'; import { DataRequestContext } from '../../../../actions'; import { StyleMetaDescriptor, @@ -52,11 +52,11 @@ export class MvtVectorLayer extends AbstractVectorLayer { return layerDescriptor; } - readonly _source: ITiledSingleLayerVectorSource; // downcast to the more specific type + readonly _source: IMvtVectorSource; constructor({ layerDescriptor, source }: VectorLayerArguments) { super({ layerDescriptor, source }); - this._source = source as ITiledSingleLayerVectorSource; + this._source = source as IMvtVectorSource; } getFeatureId(feature: Feature): string | number | undefined { @@ -180,7 +180,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { this.getSource(), this.getCurrentStyle() ), - source: this.getSource() as ITiledSingleLayerVectorSource, + source: this.getSource() as IMvtVectorSource, syncContext, }); } @@ -206,9 +206,9 @@ export class MvtVectorLayer extends AbstractVectorLayer { const mbSourceId = this.getMbSourceId(); mbMap.addSource(mbSourceId, { type: 'vector', - tiles: [sourceData.urlTemplate], - minzoom: sourceData.minSourceZoom, - maxzoom: sourceData.maxSourceZoom, + tiles: [sourceData.tileUrl], + minzoom: sourceData.tileMinZoom, + maxzoom: sourceData.tileMaxZoom, }); } @@ -236,13 +236,13 @@ export class MvtVectorLayer extends AbstractVectorLayer { return; } const sourceData = sourceDataRequest.getData() as MvtSourceData | undefined; - if (!sourceData || sourceData.layerName === '') { + if (!sourceData || sourceData.tileSourceLayer === '') { return; } - this._setMbLabelProperties(mbMap, sourceData.layerName); - this._setMbPointsProperties(mbMap, sourceData.layerName); - this._setMbLinePolygonProperties(mbMap, sourceData.layerName); + this._setMbLabelProperties(mbMap, sourceData.tileSourceLayer); + this._setMbPointsProperties(mbMap, sourceData.tileSourceLayer); + this._setMbLinePolygonProperties(mbMap, sourceData.tileSourceLayer); this._syncTooManyFeaturesProperties(mbMap); } @@ -308,9 +308,9 @@ export class MvtVectorLayer extends AbstractVectorLayer { } const isSourceDifferent = - mbTileSource.tiles?.[0] !== sourceData.urlTemplate || - mbTileSource.minzoom !== sourceData.minSourceZoom || - mbTileSource.maxzoom !== sourceData.maxSourceZoom; + mbTileSource.tiles?.[0] !== sourceData.tileUrl || + mbTileSource.minzoom !== sourceData.tileMinZoom || + mbTileSource.maxzoom !== sourceData.tileMaxZoom; if (isSourceDifferent) { return true; @@ -324,7 +324,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { if ( mbLayer && // @ts-expect-error - mbLayer.sourceLayer !== sourceData.layerName && + mbLayer.sourceLayer !== sourceData.tileSourceLayer && // @ts-expect-error mbLayer.sourceLayer !== ES_MVT_META_LAYER_NAME ) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 5b72e4c780592..eb8db6c786e29 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -281,7 +281,7 @@ describe('ESGeoGridSource', () => { }); }); - describe('ITiledSingleLayerVectorSource', () => { + describe('IMvtVectorSource', () => { const mvtGeogridSource = new ESGeoGridSource( { id: 'foobar', @@ -295,28 +295,15 @@ describe('ESGeoGridSource', () => { {} ); - it('getLayerName', () => { - expect(mvtGeogridSource.getLayerName()).toBe('aggs'); + it('getTileSourceLayer', () => { + expect(mvtGeogridSource.getTileSourceLayer()).toBe('aggs'); }); - it('getMinZoom', () => { - expect(mvtGeogridSource.getMinZoom()).toBe(0); - }); - - it('getMaxZoom', () => { - expect(mvtGeogridSource.getMaxZoom()).toBe(24); - }); - - it('getUrlTemplateWithMeta', async () => { - const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta( - vectorSourceRequestMeta - ); + it('getTileUrl', async () => { + const tileUrl = await mvtGeogridSource.getTileUrl(vectorSourceRequestMeta, '1234'); - expect(urlTemplateWithMeta.layerName).toBe('aggs'); - expect(urlTemplateWithMeta.minSourceZoom).toBe(0); - expect(urlTemplateWithMeta.maxSourceZoom).toBe(24); - expect(urlTemplateWithMeta.urlTemplate).toEqual( - "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':())))&requestType=point" + expect(tileUrl).toEqual( + "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&gridPrecision=8&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'')),'6':('0':aggs,'1':())))&requestType=point&token=1234" ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx index 8aa5758ad2572..419032132ffe8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.tsx @@ -22,7 +22,6 @@ import { GIS_API_PATH, GRID_RESOLUTION, MVT_GETGRIDTILE_API_PATH, - MVT_TOKEN_PARAM_NAME, RENDER_AS, SOURCE_TYPES, VECTOR_SHAPE_TYPE, @@ -34,8 +33,7 @@ import { registerSource } from '../source_registry'; import { LICENSED_FEATURES } from '../../../licensed_features'; import { getHttp } from '../../../kibana_services'; -import { GeoJsonWithMeta } from '../vector_source'; -import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source'; +import { GeoJsonWithMeta, IMvtVectorSource } from '../vector_source'; import { ESGeoGridSourceDescriptor, MapExtent, @@ -46,12 +44,9 @@ import { ISearchSource } from '../../../../../../../src/plugins/data/common/sear import { IndexPattern } from '../../../../../../../src/plugins/data/common'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; import { isValidStringConfig } from '../../util/valid_string_config'; -import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; type ESGeoGridSourceSyncMeta = Pick; -const ES_MVT_AGGS_LAYER_NAME = 'aggs'; - const MAX_GEOTILE_LEVEL = 29; export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { @@ -62,7 +57,7 @@ export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle defaultMessage: 'Heat map', }); -export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingleLayerVectorSource { +export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSource { static createDescriptor( descriptor: Partial ): ESGeoGridSourceDescriptor { @@ -424,15 +419,11 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle } as GeoJsonWithMeta; } - // TODO rename to getMvtSourceLayerName - getLayerName(): string { - return ES_MVT_AGGS_LAYER_NAME; + getTileSourceLayer(): string { + return 'aggs'; } - // TODO rename to getMvtUrlTemplateWithMeta - async getUrlTemplateWithMeta( - searchFilters: VectorSourceRequestMeta - ): Promise { + async getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise { const indexPattern = await this.getIndexPattern(); const searchSource = await this.makeSearchSource(searchFilters, 0); searchSource.setField('aggs', this.getValueAggsDsl(indexPattern)); @@ -447,20 +438,13 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle const requestType = this._descriptor.requestType === RENDER_AS.GRID ? RENDER_AS.GRID : RENDER_AS.POINT; - const urlTemplate = `${mvtUrlServicePath}\ + return `${mvtUrlServicePath}\ ?geometryFieldName=${this._descriptor.geoField}\ &index=${indexPattern.title}\ &gridPrecision=${this._getGeoGridPrecisionResolutionDelta()}\ &requestBody=${risonDsl}\ -&requestType=${requestType}`; - - return { - refreshTokenParamName: MVT_TOKEN_PARAM_NAME, - layerName: this.getLayerName(), - minSourceZoom: this.getMinZoom(), - maxSourceZoom: this.getMaxZoom(), - urlTemplate, - }; +&requestType=${requestType}\ +&token=${refreshToken}`; } isFilterByMapBounds(): boolean { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts index baee5b78f75f3..6bd903d6404e1 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts @@ -26,15 +26,13 @@ describe('ESSearchSource', () => { expect(esSearchSource instanceof ESSearchSource).toBe(true); }); - describe('ITiledSingleLayerVectorSource', () => { - it('mb-source params', () => { + describe('IMvtVectorSource', () => { + it('getTileSourceLayer', () => { const esSearchSource = new ESSearchSource(mockDescriptor); - expect(esSearchSource.getMinZoom()).toBe(0); - expect(esSearchSource.getMaxZoom()).toBe(24); - expect(esSearchSource.getLayerName()).toBe('hits'); + expect(esSearchSource.getTileSourceLayer()).toBe('hits'); }); - describe('getUrlTemplateWithMeta', () => { + describe('getTileUrl', () => { const geoFieldName = 'bar'; const mockIndexPatternService = { get() { @@ -115,9 +113,9 @@ describe('ESSearchSource', () => { geoField: geoFieldName, indexPatternId: 'ipId', }); - const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta(searchFilters); - expect(urlTemplateWithMeta.urlTemplate).toBe( - `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))` + const tileUrl = await esSearchSource.getTileUrl(searchFilters, '1234'); + expect(tileUrl).toBe( + `rootdir/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&token=1234` ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index f938e691c8e4f..092b021815bfd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -35,7 +35,6 @@ import { FIELD_ORIGIN, GIS_API_PATH, MVT_GETTILE_API_PATH, - MVT_TOKEN_PARAM_NAME, SCALING_TYPES, SOURCE_TYPES, VECTOR_SHAPE_TYPE, @@ -60,14 +59,12 @@ import { } from '../../../../../../../src/plugins/data/common'; import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; -import { GeoJsonWithMeta, SourceStatus } from '../vector_source'; -import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source'; +import { GeoJsonWithMeta, IMvtVectorSource, SourceStatus } from '../vector_source'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { DataRequest } from '../../util/data_request'; import { isValidStringConfig } from '../../util/valid_string_config'; import { TopHitsUpdateSourceEditor } from './top_hits'; import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields'; -import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; import { addFeatureToIndex, deleteFeatureFromIndex, @@ -85,8 +82,6 @@ type ESSearchSourceSyncMeta = Pick< | 'topHitsSize' >; -const ES_MVT_HITS_LAYER_NAME = 'hits'; - export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { const timeRangeBounds = getTimeFilter().calculateBounds(timerange); return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined @@ -101,7 +96,7 @@ export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); -export class ESSearchSource extends AbstractESSource implements ITiledSingleLayerVectorSource { +export class ESSearchSource extends AbstractESSource implements IMvtVectorSource { readonly _descriptor: ESSearchSourceDescriptor; protected readonly _tooltipFields: ESDocField[]; @@ -764,10 +759,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return reason; } - getLayerName(): string { - return ES_MVT_HITS_LAYER_NAME; - } - async _getEditableIndex(): Promise { const indexList = await this.getSourceIndexList(); if (indexList.length === 0) { @@ -800,9 +791,11 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye await deleteFeatureFromIndex(index, featureId); } - async getUrlTemplateWithMeta( - searchFilters: VectorSourceRequestMeta - ): Promise { + getTileSourceLayer(): string { + return 'hits'; + } + + async getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise { const indexPattern = await this.getIndexPattern(); const indexSettings = await loadIndexSettings(indexPattern.title); @@ -836,18 +829,11 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye `/${GIS_API_PATH}/${MVT_GETTILE_API_PATH}/{z}/{x}/{y}.pbf` ); - const urlTemplate = `${mvtUrlServicePath}\ + return `${mvtUrlServicePath}\ ?geometryFieldName=${this._descriptor.geoField}\ &index=${indexPattern.title}\ -&requestBody=${risonDsl}`; - - return { - refreshTokenParamName: MVT_TOKEN_PARAM_NAME, - layerName: this.getLayerName(), - minSourceZoom: this.getMinZoom(), - maxSourceZoom: this.getMaxZoom(), - urlTemplate, - }; +&requestBody=${risonDsl}\ +&token=${refreshToken}`; } async getTimesliceMaskFieldName(): Promise { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx index b265c4883323e..68c94351a3c04 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx @@ -19,14 +19,11 @@ const descriptor: TiledSingleLayerVectorSourceDescriptor = { tooltipProperties: [], }; -describe('getUrlTemplateWithMeta', () => { - it('should echo configuration', async () => { +describe('IMvtVectorSoucegetTileUrl', () => { + it('getTileUrl', async () => { const source = new MVTSingleLayerVectorSource(descriptor); - const config = await source.getUrlTemplateWithMeta(); - expect(config.urlTemplate).toEqual(descriptor.urlTemplate); - expect(config.layerName).toEqual(descriptor.layerName); - expect(config.minSourceZoom).toEqual(descriptor.minSourceZoom); - expect(config.maxSourceZoom).toEqual(descriptor.maxSourceZoom); + const tileUrl = await source.getTileUrl(); + expect(tileUrl).toEqual(descriptor.urlTemplate); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index b2e846a24a2fe..b3ded0620b256 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -10,8 +10,7 @@ import uuid from 'uuid/v4'; import React from 'react'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; import { AbstractSource, ImmutableSourceProperty, SourceEditorArgs } from '../source'; -import { BoundsRequestMeta, GeoJsonWithMeta } from '../vector_source'; -import { ITiledSingleLayerVectorSource } from '../tiled_single_layer_vector_source'; +import { BoundsRequestMeta, GeoJsonWithMeta, IMvtVectorSource } from '../vector_source'; import { FIELD_ORIGIN, MAX_ZOOM, @@ -30,7 +29,6 @@ import { MVTField } from '../../fields/mvt_field'; import { UpdateSourceEditor } from './update_source_editor'; import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; -import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; export const sourceTitle = i18n.translate( 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', @@ -39,10 +37,7 @@ export const sourceTitle = i18n.translate( } ); -export class MVTSingleLayerVectorSource - extends AbstractSource - implements ITiledSingleLayerVectorSource -{ +export class MVTSingleLayerVectorSource extends AbstractSource implements IMvtVectorSource { static createDescriptor({ urlTemplate, layerName, @@ -145,7 +140,7 @@ export class MVTSingleLayerVectorSource } getGeoJsonWithMeta(): Promise { - // Having this method here is a consequence of ITiledSingleLayerVectorSource extending IVectorSource. + // Having this method here is a consequence of IMvtVectorSource extending IVectorSource. throw new Error('Does not implement getGeoJsonWithMeta'); } @@ -153,7 +148,7 @@ export class MVTSingleLayerVectorSource return this.getMVTFields(); } - getLayerName(): string { + getTileSourceLayer(): string { return this._descriptor.layerName; } @@ -165,16 +160,11 @@ export class MVTSingleLayerVectorSource } async getDisplayName(): Promise { - return this.getLayerName(); + return this.getTileSourceLayer(); } - async getUrlTemplateWithMeta(): Promise { - return { - urlTemplate: this._descriptor.urlTemplate, - layerName: this._descriptor.layerName, - minSourceZoom: this._descriptor.minSourceZoom, - maxSourceZoom: this._descriptor.maxSourceZoom, - }; + async getTileUrl(): Promise { + return this._descriptor.urlTemplate; } async getSupportedShapeTypes(): Promise { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx index 016879858faab..b509c8f124b90 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx @@ -33,7 +33,7 @@ export class UpdateSourceEditor extends Component { _handleChange = (settings: MVTSettings) => { const changes: OnSourceChangeArgs[] = []; - if (settings.layerName !== this.props.source.getLayerName()) { + if (settings.layerName !== this.props.source.getTileSourceLayer()) { changes.push({ propName: 'layerName', value: settings.layerName }); } if (settings.minSourceZoom !== this.props.source.getMinZoom()) { @@ -87,7 +87,7 @@ export class UpdateSourceEditor extends Component { ; - getMinZoom(): number; - getMaxZoom(): number; - getLayerName(): string; -} diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/index.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/index.ts index a954b8b154601..f2f834d1d55d0 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/index.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/index.ts @@ -6,3 +6,4 @@ */ export * from './vector_source'; +export type { IMvtVectorSource } from './mvt_vector_source'; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts new file mode 100644 index 0000000000000..8d2c9cfede561 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/mvt_vector_source.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { IVectorSource } from '../vector_source'; + +export interface IMvtVectorSource extends IVectorSource { + /* + * IMvtVectorSource.getTileUrl returns the tile source URL. + * Append refreshToken as a URL parameter to force tile re-fetch on refresh (not required) + */ + getTileUrl(searchFilters: VectorSourceRequestMeta, refreshToken: string): Promise; + + /* + * Tile vector sources can contain multiple layers. For example, elasticsearch _mvt tiles contain the layers "hits", "aggs", and "meta". + * Use getTileSourceLayer to specify the displayed source layer. + */ + getTileSourceLayer(): string; +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/maki_icons.ts b/x-pack/plugins/maps/public/classes/styles/vector/maki_icons.ts new file mode 100644 index 0000000000000..3a5e78fc2ea73 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/maki_icons.ts @@ -0,0 +1,729 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const MAKI_ICONS = { + aerialway: { + label: 'Aerialway', + svg: '\n\n \n', + }, + airfield: { + label: 'Airfield', + svg: '\n\n \n', + }, + airport: { + label: 'Airport', + svg: '\n\n \n', + }, + 'alcohol-shop': { + label: 'Alcohol shop', + svg: '\n\n \n', + }, + 'american-football': { + label: 'American football', + svg: '\n\n \n', + }, + 'amusement-park': { + label: 'Amusement park', + svg: '\n\n \n', + }, + aquarium: { + label: 'Aquarium', + svg: '\n\n \n', + }, + 'arrow-es': { + label: 'Arrow', + svg: '\n\n \n', + }, + 'art-gallery': { + label: 'Art gallery', + svg: '\n\n \n', + }, + attraction: { + label: 'Attraction', + svg: '\n\n \n', + }, + bakery: { + label: 'Bakery', + svg: '\n\n \n \n', + }, + bank: { + label: 'Bank', + svg: '\n\n \n', + }, + bar: { + label: 'Bar', + svg: '\n\n \n', + }, + barrier: { + label: 'Barrier', + svg: '\n\n \n', + }, + baseball: { + label: 'Baseball', + svg: '\n\n \n', + }, + basketball: { + label: 'Basketball', + svg: '\n\n \n', + }, + bbq: { + label: 'BBQ', + svg: '\n\n \n', + }, + beach: { + label: 'Beach', + svg: '\n\n \n', + }, + beer: { + label: 'Beer', + svg: '\n\n \n', + }, + bicycle: { + label: 'Bicycle', + svg: '\n\n \n', + }, + 'bicycle-share': { + label: 'Bicycle share', + svg: '\n\n \n', + }, + 'blood-bank': { + label: 'Blood bank', + svg: '\n\n \n', + }, + 'boat-es': { + label: 'Boat', + svg: '\n\n \n', + }, + 'bowling-alley': { + label: 'Bowling alley', + svg: '\n\n \n', + }, + bridge: { + label: 'Bridge', + svg: '\n\n \n', + }, + building: { + label: 'Building', + svg: '\n\n \n', + }, + 'building-alt1': { + label: 'Building 2', + svg: '\n\n \n', + }, + bus: { + label: 'Bus', + svg: '\n\n \n', + }, + cafe: { + label: 'Cafe', + svg: '\n\n \n', + }, + campsite: { + label: 'Campsite', + svg: '\n\n \n', + }, + car: { + label: 'Car', + svg: '\n\n \n', + }, + 'car-top-es': { + label: 'Car 2', + svg: '\n\n \n', + }, + 'car-rental': { + label: 'Car rental', + svg: '\n\n \n \n \n \n', + }, + 'car-repair': { + label: 'Car repair', + svg: '\n\n \n \n \n \n', + }, + casino: { + label: 'Casino', + svg: '\n\n \n', + }, + castle: { + label: 'Castle', + svg: '\n\n \n', + }, + cemetery: { + label: 'Cemetery', + svg: '\n\n \n', + }, + 'charging-station': { + label: 'Charging station', + svg: '\n\n \n', + }, + cinema: { + label: 'Cinema', + svg: '\n\n \n', + }, + circle: { + label: 'Circle', + svg: '\n\n \n', + }, + 'circle-stroked': { + label: 'Circle stroked', + svg: '\n\n \n', + }, + city: { + label: 'City', + svg: '\n\n \n', + }, + 'clothing-store': { + label: 'Clothing store', + svg: '\n\n \n', + }, + college: { + label: 'College', + svg: '\n\n \n', + }, + commercial: { + label: 'Commercial', + svg: '\n\n \n', + }, + 'communications-tower': { + label: 'Communications tower', + svg: '\n\n \n \n \n', + }, + confectionery: { + label: 'Confectionery', + svg: '\n\n \n \n \n', + }, + convenience: { + label: 'Convenience', + svg: '\n\n \n \n \n', + }, + cricket: { + label: 'Cricket', + svg: '\n\n \n', + }, + cross: { + label: 'Cross', + svg: '\n\n \n', + }, + dam: { + label: 'Dam', + svg: '\n\n \n', + }, + danger: { + label: 'Danger', + svg: '\n\n \n', + }, + defibrillator: { + label: 'Defibrillator', + svg: '\n\n \n', + }, + dentist: { + label: 'Dentist', + svg: '\n\n \n', + }, + doctor: { + label: 'Doctor', + svg: '\n\n \n', + }, + 'dog-park': { + label: 'Dog park', + svg: '\n\n \n \n \n', + }, + 'drinking-water': { + label: 'Drinking water', + svg: '\n\n \n \n', + }, + embassy: { + label: 'Embassy', + svg: '\n\n \n', + }, + 'emergency-phone': { + label: 'Emergency phone', + svg: '\n\n \n', + }, + entrance: { + label: 'Entrance', + svg: '\n\n \n \n', + }, + 'entrance-alt1': { + label: 'Entrance 2', + svg: '\n\n \n', + }, + farm: { + label: 'Farm', + svg: '\n\n \n', + }, + 'fast-food': { + label: 'Fast food', + svg: '\n\n \n', + }, + fence: { + label: 'Fence', + svg: '\n\n \n', + }, + ferry: { + label: 'Ferry', + svg: '\n\n \n', + }, + 'fire-station': { + label: 'Fire station', + svg: '\n\n \n', + }, + 'fitness-centre': { + label: 'Fitness centre', + svg: '\n\n \n', + }, + florist: { + label: 'Florist', + svg: '\n\n \n', + }, + fuel: { + label: 'Fuel', + svg: '\n\n \n', + }, + furniture: { + label: 'Furniture', + svg: '\n\n \n \n \n \n', + }, + gaming: { + label: 'Gaming', + svg: '\n\n \n', + }, + garden: { + label: 'Garden', + svg: '\n\n \n', + }, + 'garden-centre': { + label: 'Garden centre', + svg: '\n\n \n', + }, + gift: { + label: 'Gift', + svg: '\n\n \n', + }, + globe: { + label: 'Globe', + svg: '\n\n \n \n \n \n \n \n \n \n', + }, + golf: { + label: 'Golf', + svg: '\n\n \n', + }, + grocery: { + label: 'Grocery', + svg: '\n\n \n \n \n', + }, + hairdresser: { + label: 'Hairdresser', + svg: '\n\n \n', + }, + harbor: { + label: 'Harbor', + svg: '\n\n \n', + }, + hardware: { + label: 'Hardware', + svg: '\n\n \n', + }, + heart: { + label: 'Heart', + svg: '\n\n \n', + }, + heliport: { + label: 'Heliport', + svg: '\n\n \n', + }, + home: { + label: 'Home', + svg: '\n\n \n', + }, + 'horse-riding': { + label: 'Horse riding', + svg: '\n\n \n', + }, + hospital: { + label: 'Hospital', + svg: '\n\n \n', + }, + 'ice-cream': { + label: 'Ice cream', + svg: '\n\n \n \n', + }, + industry: { + label: 'Industry', + svg: '\n\n \n', + }, + information: { + label: 'Information', + svg: '\n\n \n', + }, + 'jewelry-store': { + label: 'Jewelry store', + svg: '\n\n \n', + }, + karaoke: { + label: 'Karaoke', + svg: '\n\n \n \n \n \n', + }, + landmark: { + label: 'Landmark', + svg: '\n\n \n', + }, + landuse: { + label: 'Landuse', + svg: '\n\n \n', + }, + laundry: { + label: 'Laundry', + svg: '\n\n \n', + }, + library: { + label: 'Library', + svg: '\n\n \n', + }, + lighthouse: { + label: 'Lighthouse', + svg: '\n\n \n', + }, + lodging: { + label: 'Lodging', + svg: '\n\n \n', + }, + logging: { + label: 'Logging', + svg: '\n\n \n', + }, + marker: { + label: 'Marker', + svg: '\n\n \n', + }, + 'marker-stroked': { + label: 'Marker stroked', + svg: '\n\n \n', + }, + 'mobile-phone': { + label: 'Mobile phone', + svg: '\n\n \n', + }, + monument: { + label: 'Monument', + svg: '\n\n \n', + }, + mountain: { + label: 'Mountain', + svg: '\n\n \n', + }, + museum: { + label: 'Museum', + svg: '\n\n \n', + }, + music: { + label: 'Music', + svg: '\n\n \n', + }, + natural: { + label: 'Natural', + svg: '\n\n \n', + }, + 'oil-rig-es': { + label: 'Oil rig', + svg: '\n\n \n', + }, + optician: { + label: 'Optician', + svg: '\n\n \n', + }, + paint: { + label: 'Paint', + svg: '\n\n \n', + }, + park: { + label: 'Park', + svg: '\n\n \n', + }, + 'park-alt1': { + label: 'Park 2', + svg: '\n\n \n', + }, + parking: { + label: 'Parking', + svg: '\n\n \n', + }, + 'parking-garage': { + label: 'Parking garage', + svg: '\n\n \n', + }, + pharmacy: { + label: 'Pharmacy', + svg: '\n\n \n', + }, + 'picnic-site': { + label: 'Picnic site', + svg: '\n\n \n', + }, + pitch: { + label: 'Pitch', + svg: '\n\n \n', + }, + 'place-of-worship': { + label: 'Place of worship', + svg: '\n\n \n', + }, + playground: { + label: 'Playground', + svg: '\n\n \n', + }, + police: { + label: 'Police', + svg: '\n\n \n', + }, + post: { + label: 'Post', + svg: '\n\n \n', + }, + prison: { + label: 'Prison', + svg: '\n\n \n', + }, + rail: { + label: 'Rail', + svg: '\n\n \n', + }, + 'rail-light': { + label: 'Rail light', + svg: '\n\n \n', + }, + 'rail-metro': { + label: 'Rail metro', + svg: '\n\n \n', + }, + 'ranger-station': { + label: 'Ranger station', + svg: '\n\n \n', + }, + recycling: { + label: 'Recycling', + svg: '\n\n \n', + }, + 'religious-buddhist': { + label: 'Religious buddhist', + svg: '\n\n \n', + }, + 'religious-christian': { + label: 'Religious christian', + svg: '\n\n \n', + }, + 'religious-jewish': { + label: 'Religious jewish', + svg: '\n\n \n', + }, + 'religious-muslim': { + label: 'Religious muslim', + svg: '\n\n \n', + }, + 'residential-community': { + label: 'Residential community', + svg: '\n\n \n', + }, + restaurant: { + label: 'Restaurant', + svg: '\n\n \n', + }, + 'restaurant-noodle': { + label: 'Restaurant noodle', + svg: '\n\n \n \n \n', + }, + 'restaurant-pizza': { + label: 'Restaurant pizza', + svg: '\n\n \n \n \n', + }, + 'restaurant-seafood': { + label: 'Restaurant seafood', + svg: '\n\n \n \n \n', + }, + roadblock: { + label: 'Roadblock', + svg: '\n\n \n', + }, + rocket: { + label: 'Rocket', + svg: '\n\n \n', + }, + school: { + label: 'School', + svg: '\n\n \n', + }, + scooter: { + label: 'Scooter', + svg: '\n\n \n', + }, + shelter: { + label: 'Shelter', + svg: '\n\n \n', + }, + shoe: { + label: 'Shoe', + svg: '\n\n \n \n \n \n', + }, + shop: { + label: 'Shop', + svg: '\n\n \n', + }, + skateboard: { + label: 'Skateboard', + svg: '\n\n \n', + }, + skiing: { + label: 'Skiing', + svg: '\n\n \n', + }, + slaughterhouse: { + label: 'Slaughterhouse', + svg: '\n\n \n', + }, + slipway: { + label: 'Slipway', + svg: '\n\n \n \n \n', + }, + snowmobile: { + label: 'Snowmobile', + svg: '\n\n \n', + }, + soccer: { + label: 'Soccer', + svg: '\n\n \n', + }, + square: { + label: 'Square', + svg: '\n\n \n', + }, + 'square-stroked': { + label: 'Square stroked', + svg: '\n\n \n', + }, + stadium: { + label: 'Stadium', + svg: '\n\n \n', + }, + star: { + label: 'Star', + svg: '\n\n \n', + }, + 'star-stroked': { + label: 'Star stroked', + svg: '\n\n \n', + }, + suitcase: { + label: 'Suitcase', + svg: '\n\n \n', + }, + sushi: { + label: 'Sushi', + svg: '\n\n \n', + }, + swimming: { + label: 'Swimming', + svg: '\n\n \n', + }, + 'table-tennis': { + label: 'Table tennis', + svg: '\n\n \n', + }, + teahouse: { + label: 'Teahouse', + svg: '\n\n \n', + }, + telephone: { + label: 'Telephone', + svg: '\n\n \n', + }, + tennis: { + label: 'Tennis', + svg: '\n\n \n', + }, + theatre: { + label: 'Theatre', + svg: '\n\n \n', + }, + toilet: { + label: 'Toilet', + svg: '\n\n \n', + }, + town: { + label: 'Town', + svg: '\n\n \n', + }, + 'town-hall': { + label: 'Town hall', + svg: '\n\n \n', + }, + triangle: { + label: 'Triangle', + svg: '\n\n \n', + }, + 'triangle-stroked': { + label: 'Triangle stroked', + svg: '\n\n \n', + }, + veterinary: { + label: 'Veterinary', + svg: '\n\n \n \n \n \n \n', + }, + viewpoint: { + label: 'Viewpoint', + svg: '\n\n \n', + }, + village: { + label: 'Village', + svg: '\n\n \n', + }, + volcano: { + label: 'Volcano', + svg: '\n\n \n', + }, + volleyball: { + label: 'Volleyball', + svg: '\n\n \n', + }, + warehouse: { + label: 'Warehouse', + svg: '\n\n \n', + }, + 'waste-basket': { + label: 'Waste basket', + svg: '\n\n \n', + }, + watch: { + label: 'Watch', + svg: '\n\n \n \n', + }, + water: { + label: 'Water', + svg: '\n\n \n', + }, + waterfall: { + label: 'Waterfall', + svg: '\n\n \n', + }, + watermill: { + label: 'Watermill', + svg: '\n\n \n', + }, + wetland: { + label: 'Wetland', + svg: '\n\n \n', + }, + wheelchair: { + label: 'Wheelchair', + svg: '\n\n \n', + }, + windmill: { + label: 'Windmill', + svg: '\n\n \n', + }, + zoo: { + label: 'Zoo', + svg: '\n\n \n', + }, +}; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 08ad93c5b8cb7..b3e5293d6860f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -73,14 +73,14 @@ describe('get mapbox icon-image expression (via internal _getMbIconImageExpressi const iconStyle = makeProperty({ iconPaletteId: 'filledShapes', }); - expect(iconStyle._getMbIconImageExpression(15)).toEqual([ + expect(iconStyle._getMbIconImageExpression()).toEqual([ 'match', ['to-string', ['get', 'foobar']], 'US', - 'circle-15', + 'circle', 'CN', - 'marker-15', - 'square-15', + 'marker', + 'square', ]); }); @@ -92,12 +92,12 @@ describe('get mapbox icon-image expression (via internal _getMbIconImageExpressi { stop: 'MX', icon: 'marker' }, ], }); - expect(iconStyle._getMbIconImageExpression(15)).toEqual([ + expect(iconStyle._getMbIconImageExpression()).toEqual([ 'match', ['to-string', ['get', 'foobar']], 'MX', - 'marker-15', - 'circle-15', + 'marker', + 'circle', ]); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx index b5d5e90efa45f..77510f9c82d0b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx @@ -10,8 +10,11 @@ import React from 'react'; import { EuiTextColor } from '@elastic/eui'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; -// @ts-expect-error -import { getIconPalette, getMakiIconId, getMakiSymbolAnchor } from '../symbol_utils'; +import { + getIconPalette, + getMakiSymbolAnchor, + // @ts-expect-error +} from '../symbol_utils'; import { BreakedLegend } from '../components/legend/breaked_legend'; import { getOtherCategoryLabel, assignCategoriesToPalette } from '../style_util'; import { LegendProps } from './style_property'; @@ -31,13 +34,9 @@ export class DynamicIconProperty extends DynamicStyleProperty { mbStops.push(`${stop}`); - mbStops.push(getMakiIconId(style, iconPixelSize)); + mbStops.push(style); }); if (fallbackSymbolId) { - mbStops.push(getMakiIconId(fallbackSymbolId, iconPixelSize)); // last item is fallback style for anything that does not match provided stops + mbStops.push(fallbackSymbolId); // last item is fallback style for anything that does not match provided stops } return ['match', ['to-string', ['get', this.getMbFieldName()]], ...mbStops]; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx index e76e9e936faec..5ea99e64e8626 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx @@ -11,9 +11,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { OrdinalLegend } from '../components/legend/ordinal_legend'; import { makeMbClampedNumberExpression } from '../style_util'; import { - HALF_LARGE_MAKI_ICON_SIZE, - LARGE_MAKI_ICON_SIZE, - SMALL_MAKI_ICON_SIZE, + HALF_MAKI_ICON_SIZE, // @ts-expect-error } from '../symbol_utils'; import { FieldFormatter, MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; @@ -55,16 +53,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty= HALF_LARGE_MAKI_ICON_SIZE - ? LARGE_MAKI_ICON_SIZE - : SMALL_MAKI_ICON_SIZE; - } - syncIconSizeWithMb(symbolLayerId: string, mbMap: MbMap) { const rangeFieldMeta = this.getRangeFieldMeta(); if (this._isSizeDynamicConfigComplete() && rangeFieldMeta) { - const halfIconPixels = this.getIconPixelSize() / 2; const targetName = this.getMbFieldName(); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [ @@ -78,9 +69,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { - syncIconWithMb(symbolLayerId: string, mbMap: MbMap, iconPixelSize: number) { + syncIconWithMb(symbolLayerId: string, mbMap: MbMap) { const symbolId = this._options.value; mbMap.setLayoutProperty(symbolLayerId, 'icon-anchor', getMakiSymbolAnchor(symbolId)); - mbMap.setLayoutProperty(symbolLayerId, 'icon-image', getMakiIconId(symbolId, iconPixelSize)); + mbMap.setLayoutProperty(symbolLayerId, 'icon-image', symbolId); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts index de71d07aa7167..771e0f8f33a0c 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts @@ -9,9 +9,7 @@ import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; import { VECTOR_STYLES } from '../../../../../common/constants'; import { - HALF_LARGE_MAKI_ICON_SIZE, - LARGE_MAKI_ICON_SIZE, - SMALL_MAKI_ICON_SIZE, + HALF_MAKI_ICON_SIZE, // @ts-expect-error } from '../symbol_utils'; import { SizeStaticOptions } from '../../../../../common/descriptor_types'; @@ -29,15 +27,8 @@ export class StaticSizeProperty extends StaticStyleProperty { mbMap.setPaintProperty(mbLayerId, 'icon-halo-width', this._options.size); } - getIconPixelSize() { - return this._options.size >= HALF_LARGE_MAKI_ICON_SIZE - ? LARGE_MAKI_ICON_SIZE - : SMALL_MAKI_ICON_SIZE; - } - syncIconSizeWithMb(symbolLayerId: string, mbMap: MbMap) { - const halfIconPixels = this.getIconPixelSize() / 2; - mbMap.setLayoutProperty(symbolLayerId, 'icon-size', this._options.size / halfIconPixels); + mbMap.setLayoutProperty(symbolLayerId, 'icon-size', this._options.size / HALF_MAKI_ICON_SIZE); } syncCircleStrokeWidthWithMb(mbLayerId: string, mbMap: MbMap, hasNoRadius: boolean) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js index 30cc93d65722b..07ac77dc0cb78 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js @@ -6,46 +6,76 @@ */ import React from 'react'; -import maki from '@elastic/maki'; import xml2js from 'xml2js'; +import { Canvg } from 'canvg'; +import calcSDF from 'bitmap-sdf'; import { parseXmlString } from '../../../../common/parse_xml_string'; import { SymbolIcon } from './components/legend/symbol_icon'; import { getIsDarkMode } from '../../../kibana_services'; +import { MAKI_ICONS } from './maki_icons'; -export const LARGE_MAKI_ICON_SIZE = 15; -const LARGE_MAKI_ICON_SIZE_AS_STRING = LARGE_MAKI_ICON_SIZE.toString(); -export const SMALL_MAKI_ICON_SIZE = 11; -export const HALF_LARGE_MAKI_ICON_SIZE = Math.ceil(LARGE_MAKI_ICON_SIZE); +const MAKI_ICON_SIZE = 16; +export const HALF_MAKI_ICON_SIZE = MAKI_ICON_SIZE / 2; -export const SYMBOLS = {}; -maki.svgArray.forEach((svgString) => { - const ID_FRAG = 'id="'; - const index = svgString.indexOf(ID_FRAG); - if (index !== -1) { - const idStartIndex = index + ID_FRAG.length; - const idEndIndex = svgString.substring(idStartIndex).indexOf('"') + idStartIndex; - const fullSymbolId = svgString.substring(idStartIndex, idEndIndex); - const symbolId = fullSymbolId.substring(0, fullSymbolId.length - 3); // remove '-15' or '-11' from id - const symbolSize = fullSymbolId.substring(fullSymbolId.length - 2); // grab last 2 chars from id - // only show large icons, small/large icon selection will based on configured size style - if (symbolSize === LARGE_MAKI_ICON_SIZE_AS_STRING) { - SYMBOLS[symbolId] = svgString; - } - } -}); - -export const SYMBOL_OPTIONS = Object.keys(SYMBOLS).map((symbolId) => { +export const SYMBOL_OPTIONS = Object.keys(MAKI_ICONS).map((symbolId) => { return { value: symbolId, label: symbolId, }; }); +/** + * Converts a SVG icon to a monochrome image using a signed distance function. + * + * @param {string} svgString - SVG icon as string + * @param {number} [cutoff=0.25] - balance between SDF inside 1 and outside 0 of glyph + * @param {number} [radius=0.25] - size of SDF around the cutoff as percent of output icon size + * @return {ImageData} Monochrome image that can be added to a MapLibre map + */ +export async function createSdfIcon(svgString, cutoff = 0.25, radius = 0.25) { + const buffer = 3; + const size = MAKI_ICON_SIZE + buffer * 4; + const svgCanvas = document.createElement('canvas'); + svgCanvas.width = size; + svgCanvas.height = size; + const svgCtx = svgCanvas.getContext('2d'); + const v = Canvg.fromString(svgCtx, svgString, { + ignoreDimensions: true, + offsetX: buffer / 2, + offsetY: buffer / 2, + }); + v.resize(size - buffer, size - buffer); + await v.render(); + + const distances = calcSDF(svgCtx, { + channel: 3, + cutoff, + radius: radius * size, + }); + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + + const imageData = ctx.createImageData(size, size); + for (let i = 0; i < size; i++) { + for (let j = 0; j < size; j++) { + imageData.data[j * size * 4 + i * 4 + 0] = 0; + imageData.data[j * size * 4 + i * 4 + 1] = 0; + imageData.data[j * size * 4 + i * 4 + 2] = 0; + imageData.data[j * size * 4 + i * 4 + 3] = distances[j * size + i] * 255; + } + } + return imageData; +} + export function getMakiSymbolSvg(symbolId) { - if (!SYMBOLS[symbolId]) { + const svg = MAKI_ICONS?.[symbolId]?.svg; + if (!svg) { throw new Error(`Unable to find symbol: ${symbolId}`); } - return SYMBOLS[symbolId]; + return svg; } export function getMakiSymbolAnchor(symbolId) { @@ -59,12 +89,6 @@ export function getMakiSymbolAnchor(symbolId) { } } -// Style descriptor stores symbolId, for example 'aircraft' -// Icons are registered in Mapbox with full maki ids, for example 'aircraft-11' -export function getMakiIconId(symbolId, iconPixelSize) { - return `${symbolId}-${iconPixelSize}`; -} - export function buildSrcUrl(svgString) { const domUrl = window.URL || window.webkitURL || window; const svg = new Blob([svgString], { type: 'image/svg+xml' }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 5afd05366ab1d..a4ea62cb63970 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -899,11 +899,7 @@ export class VectorStyle implements IVectorStyle { mbMap.setPaintProperty(symbolLayerId, 'icon-opacity', alpha); mbMap.setLayoutProperty(symbolLayerId, 'icon-allow-overlap', true); - this._iconStyleProperty.syncIconWithMb( - symbolLayerId, - mbMap, - this._iconSizeStyleProperty.getIconPixelSize() - ); + this._iconStyleProperty.syncIconWithMb(symbolLayerId, mbMap); // icon-color is only supported on SDF icons. this._fillColorStyleProperty.syncIconColorWithMb(symbolLayerId, mbMap); this._lineColorStyleProperty.syncHaloBorderColorWithMb(symbolLayerId, mbMap); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index c262eaa9d1527..7646b6033a2f5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -7,10 +7,6 @@ import _ from 'lodash'; import React, { Component } from 'react'; -// @ts-expect-error -import { spritesheet } from '@elastic/maki'; -import sprites1 from '@elastic/maki/dist/sprite@1.png'; -import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { Adapters } from 'src/plugins/inspector/public'; import { Filter } from 'src/plugins/data/public'; import { Action, ActionExecutionContext } from 'src/plugins/ui_actions/public'; @@ -33,20 +29,18 @@ import { Timeslice, } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, RawValue, ZOOM_PRECISION } from '../../../common/constants'; -import { getGlyphUrl, isRetina } from '../../util'; +import { getGlyphUrl } from '../../util'; import { syncLayerOrder } from './sort_layers'; -import { - addSpriteSheetToMapFromImageData, - getTileMetaFeatures, - loadSpriteSheetImageData, - removeOrphanedSourcesAndLayers, -} from './utils'; +import { getTileMetaFeatures, removeOrphanedSourcesAndLayers } from './utils'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { TileStatusTracker } from './tile_status_tracker'; import { DrawFeatureControl } from './draw_control/draw_feature_control'; import type { MapExtentState } from '../../reducers/map/types'; +// @ts-expect-error +import { createSdfIcon } from '../../classes/styles/vector/symbol_utils'; +import { MAKI_ICONS } from '../../classes/styles/vector/maki_icons'; export interface Props { isMapReady: boolean; @@ -290,11 +284,17 @@ export class MbMap extends Component { } async _loadMakiSprites(mbMap: MapboxMap) { - const spritesUrl = isRetina() ? sprites2 : sprites1; - const json = isRetina() ? spritesheet[2] : spritesheet[1]; - const spritesData = await loadSpriteSheetImageData(spritesUrl); if (this._isMounted) { - addSpriteSheetToMapFromImageData(json, spritesData, mbMap); + const pixelRatio = Math.floor(window.devicePixelRatio); + for (const [symbolId, { svg }] of Object.entries(MAKI_ICONS)) { + if (!mbMap.hasImage(symbolId)) { + const imageData = await createSdfIcon(svg, 0.25, 0.25); + mbMap.addImage(symbolId, imageData, { + pixelRatio, + sdf: true, + }); + } + } } } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts b/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts index f5de99d04c01c..a79c1a1f71b76 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts @@ -7,11 +7,8 @@ import type { Map as MbMap } from '@kbn/mapbox-gl'; import { TileMetaFeature } from '../../../common/descriptor_types'; -// @ts-expect-error -import { RGBAImage } from './image_utils'; import { isGlDrawLayer } from './sort_layers'; import { ILayer } from '../../classes/layers/layer'; -import { EmsSpriteSheet } from '../../classes/layers/ems_vector_tile_layer/ems_vector_tile_layer'; import { ES_MVT_META_LAYER_NAME } from '../../classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer'; export function removeOrphanedSourcesAndLayers( @@ -64,64 +61,6 @@ export function removeOrphanedSourcesAndLayers( mbSourcesToRemove.forEach((mbSourceId) => mbMap.removeSource(mbSourceId)); } -function getImageData(img: HTMLImageElement) { - const canvas = window.document.createElement('canvas'); - const context = canvas.getContext('2d'); - if (!context) { - throw new Error('failed to create canvas 2d context'); - } - canvas.width = img.width; - canvas.height = img.height; - context.drawImage(img, 0, 0, img.width, img.height); - return context.getImageData(0, 0, img.width, img.height); -} - -function isCrossOriginUrl(url: string) { - const a = window.document.createElement('a'); - a.href = url; - return ( - a.protocol !== window.document.location.protocol || - a.host !== window.document.location.host || - a.port !== window.document.location.port - ); -} - -export async function loadSpriteSheetImageData(imgUrl: string): Promise { - return new Promise((resolve, reject) => { - const image = new Image(); - if (isCrossOriginUrl(imgUrl)) { - image.crossOrigin = 'Anonymous'; - } - image.onload = (event) => { - resolve(getImageData(image)); - }; - image.onerror = (e) => { - reject(e); - }; - image.src = imgUrl; - }); -} - -export function addSpriteSheetToMapFromImageData( - json: EmsSpriteSheet, - imgData: ImageData, - mbMap: MbMap -) { - for (const imageId in json) { - if (!(json.hasOwnProperty(imageId) && !mbMap.hasImage(imageId))) { - continue; - } - const { width, height, x, y, sdf, pixelRatio } = json[imageId]; - if (typeof width !== 'number' || typeof height !== 'number') { - continue; - } - - const data = new RGBAImage({ width, height }); - RGBAImage.copy(imgData, data, { x, y }, { x: 0, y: 0 }, { width, height }); - mbMap.addImage(imageId, data, { pixelRatio, sdf }); - } -} - export function getTileMetaFeatures(mbMap: MbMap, mbSourceId: string): TileMetaFeature[] { // querySourceFeatures can return duplicated features when features cross tile boundaries. // Tile meta will never have duplicated features since by there nature, tile meta is a feature contained within a single tile diff --git a/x-pack/plugins/metrics_entities/common/index.ts b/x-pack/plugins/metrics_entities/common/index.ts index a6630f3ff67b0..f100308ef2e9b 100644 --- a/x-pack/plugins/metrics_entities/common/index.ts +++ b/x-pack/plugins/metrics_entities/common/index.ts @@ -5,10 +5,9 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110904 -/* eslint-disable @kbn/eslint/no_export_all */ - -export const PLUGIN_ID = 'metricsEntities'; -export const PLUGIN_NAME = 'metrics_entities'; - -export * from './constants'; +// Careful of exporting anything from this file as any file(s) you export here will cause your functions to be exposed as public. +// If you're using functions/types/etc... internally or within integration tests it's best to import directly from their paths +// than expose the functions/types/etc... here. You should _only_ expose functions/types/etc... that need to be shared with other plugins here. +// When you do have to add things here you might want to consider creating a package such to share with other plugins instead as packages +// are easier to break down. +// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api diff --git a/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts b/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts index 20951b0e447ff..dde4d98220549 100644 --- a/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts +++ b/x-pack/plugins/metrics_entities/server/services/utils/compute_transform_id.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ELASTIC_NAME } from '../../../common'; +import { ELASTIC_NAME } from '../../../common/constants'; export const computeTransformId = ({ prefix, diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index ea8ad43d6bb3b..c76b662df7a5a 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -15,3 +15,4 @@ export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; export { extractErrorMessage } from './util/errors'; export type { RuntimeMappings } from './types/fields'; export { getDefaultCapabilities as getDefaultMlCapabilities } from './types/capabilities'; +export { DATAFEED_STATE, JOB_STATE } from './constants/states'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx index 0a83ed6db6539..28e63d0e002aa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_analytics_advanced_editor/create_analytics_advanced_editor.tsx @@ -7,19 +7,10 @@ import React, { FC, Fragment, useEffect, useMemo, useRef } from 'react'; import { debounce } from 'lodash'; -import { - EuiCallOut, - EuiFieldText, - EuiForm, - EuiFormRow, - EuiSpacer, - EuiSwitch, - EuiText, -} from '@elastic/eui'; +import { EuiCallOut, EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../../../../contexts/kibana'; import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { useNotifications } from '../../../../../contexts/kibana'; import { ml } from '../../../../../services/ml_api_service'; @@ -34,33 +25,15 @@ export const CreateAnalyticsAdvancedEditor: FC = (prop const { advancedEditorMessages, advancedEditorRawString, isJobCreated } = state; - const { - createIndexPattern, - destinationIndexPatternTitleExists, - jobId, - jobIdEmpty, - jobIdExists, - jobIdValid, - } = state.form; + const { jobId, jobIdEmpty, jobIdExists, jobIdValid } = state.form; const forceInput = useRef(null); const { toasts } = useNotifications(); - const { - services: { - application: { capabilities }, - }, - } = useMlKibana(); const onChange = (str: string) => { setAdvancedEditorRawString(str); }; - const canCreateDataView = useMemo( - () => - capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true, - [capabilities] - ); - const debouncedJobIdCheck = useMemo( () => debounce(async () => { @@ -217,47 +190,6 @@ export const CreateAnalyticsAdvancedEditor: FC = (prop ))} - {!isJobCreated && ( - - - {i18n.translate( - 'xpack.ml.dataframe.analytics.create.dataViewPermissionWarning', - { - defaultMessage: 'You need permission to create data views.', - } - )} -
, - ] - : []), - ...(createIndexPattern && destinationIndexPatternTitleExists - ? [ - i18n.translate('xpack.ml.dataframe.analytics.create.dataViewExistsError', { - defaultMessage: 'A data view with this title already exists.', - }), - ] - : []), - ]} - > - setFormState({ createIndexPattern: !createIndexPattern })} - /> - - - )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx index 77c00a94227f0..19b1570d1cf63 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useState } from 'react'; +import React, { FC, useEffect, useMemo, useState } from 'react'; import { EuiButton, EuiCheckbox, @@ -13,9 +13,11 @@ import { EuiFlexItem, EuiFormRow, EuiSpacer, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../../contexts/kibana'; import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; import { Messages } from '../shared'; import { ANALYTICS_STEPS } from '../../page'; @@ -26,14 +28,38 @@ interface Props extends CreateAnalyticsFormProps { } export const CreateStep: FC = ({ actions, state, step }) => { - const { createAnalyticsJob, startAnalyticsJob } = actions; + const { + services: { + application: { capabilities }, + }, + } = useMlKibana(); + + const canCreateDataView = useMemo( + () => + capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true, + [capabilities] + ); + + const { createAnalyticsJob, setFormState, startAnalyticsJob } = actions; const { isAdvancedEditorValidJson, isJobCreated, isJobStarted, isValid, requestMessages } = state; - const { jobId, jobType } = state.form; + const { + createIndexPattern, + destinationIndex, + destinationIndexPatternTitleExists, + jobId, + jobType, + } = state.form; - const [checked, setChecked] = useState(true); + const [startChecked, setStartChecked] = useState(true); const [creationTriggered, setCreationTriggered] = useState(false); const [showProgress, setShowProgress] = useState(false); + useEffect(() => { + if (canCreateDataView === false) { + setFormState({ createIndexPattern: false }); + } + }, [capabilities]); + if (step !== ANALYTICS_STEPS.CREATE) return null; const handleCreation = async () => { @@ -44,7 +70,7 @@ export const CreateStep: FC = ({ actions, state, step }) => { setCreationTriggered(false); } - if (checked && creationSuccess === true) { + if (startChecked && creationSuccess === true) { setShowProgress(true); startAnalyticsJob(); } @@ -53,32 +79,114 @@ export const CreateStep: FC = ({ actions, state, step }) => { return (
{!isJobCreated && !isJobStarted && ( - + - - setChecked(e.target.checked)} - /> - + + + + { + setStartChecked(e.target.checked); + if (e.target.checked === false) { + setFormState({ createIndexPattern: false }); + } + }} + /> + + + {startChecked ? ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.dataViewPermissionWarning', + { + defaultMessage: 'You need permission to create data views.', + } + )} + , + ] + : []), + ...(createIndexPattern && destinationIndexPatternTitleExists + ? [ + i18n.translate( + 'xpack.ml.dataframe.analytics.create.dataViewExistsError', + { + defaultMessage: + 'A data view with the title {title} already exists.', + values: { title: destinationIndex }, + } + ), + ] + : []), + ...(!createIndexPattern && !destinationIndexPatternTitleExists + ? [ + + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.shouldCreateDataViewMessage', + { + defaultMessage: + 'You may not be able to view job results if a data view is not created for the destination index.', + } + )} + , + ] + : []), + ]} + > + setFormState({ createIndexPattern: !createIndexPattern })} + data-test-subj="mlAnalyticsCreateJobWizardCreateIndexPatternCheckbox" + /> + + + ) : null} + = ({ actions, state, step }) => { )} - {isJobCreated === true && ( + {isJobCreated === true ? ( - )} + ) : null}
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx index 3123a43594c93..f8016bccb1832 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step_footer/create_step_footer.tsx @@ -54,6 +54,8 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => }, []); useEffect(() => { + if (showProgress === false) return; + const interval = setInterval(async () => { try { const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); @@ -129,11 +131,11 @@ export const CreateStepFooter: FC = ({ jobId, jobType, showProgress }) => - {jobFinished === true && ( + {jobFinished === true ? ( - )} + ) : null} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index d3b4c27e16e78..6e702c8ab6d1d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -7,15 +7,7 @@ import React, { FC, Fragment, useRef, useEffect, useMemo, useState } from 'react'; import { debounce } from 'lodash'; -import { - EuiFieldText, - EuiFormRow, - EuiLink, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTextArea, -} from '@elastic/eui'; +import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useMlKibana } from '../../../../../contexts/kibana'; @@ -42,23 +34,17 @@ export const DetailsStepForm: FC = ({ setCurrentStep, }) => { const { - services: { - docLinks, - notifications, - application: { capabilities }, - }, + services: { docLinks, notifications }, } = useMlKibana(); const createIndexLink = docLinks.links.apis.createIndex; const { setFormState } = actions; const { form, cloneJob, hasSwitchedToEditor, isJobCreated } = state; const { - createIndexPattern, description, destinationIndex, destinationIndexNameEmpty, destinationIndexNameExists, destinationIndexNameValid, - destinationIndexPatternTitleExists, jobId, jobIdEmpty, jobIdExists, @@ -75,11 +61,6 @@ export const DetailsStepForm: FC = ({ (cloneJob !== undefined && resultsField === DEFAULT_RESULTS_FIELD) ); - const canCreateDataView = useMemo( - () => - capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true, - [capabilities] - ); const forceInput = useRef(null); const isStepInvalid = @@ -87,8 +68,7 @@ export const DetailsStepForm: FC = ({ jobIdExists === true || jobIdValid === false || destinationIndexNameEmpty === true || - destinationIndexNameValid === false || - (destinationIndexPatternTitleExists === true && createIndexPattern === true); + destinationIndexNameValid === false; const debouncedIndexCheck = debounce(async () => { try { @@ -158,12 +138,6 @@ export const DetailsStepForm: FC = ({ } }, [destIndexSameAsId, jobId]); - useEffect(() => { - if (canCreateDataView === false) { - setFormState({ createIndexPattern: false }); - } - }, [capabilities]); - return ( = ({ /> )} - - {i18n.translate('xpack.ml.dataframe.analytics.create.dataViewPermissionWarning', { - defaultMessage: 'You need permission to create data views.', - })} - , - ] - : []), - ...(createIndexPattern && destinationIndexPatternTitleExists - ? [ - i18n.translate('xpack.ml.dataframe.analytics.create.dataViewExistsError', { - defaultMessage: 'A data view with this title already exists.', - }), - ] - : []), - ...(!createIndexPattern - ? [ - - {i18n.translate( - 'xpack.ml.dataframe.analytics.create.shouldCreateDataViewMessage', - { - defaultMessage: - 'You may not be able to view job results if a data view is not created for the destination index.', - } - )} - , - ] - : []), - ]} - > - setFormState({ createIndexPattern: !createIndexPattern })} - data-test-subj="mlAnalyticsCreateJobWizardCreateIndexPatternSwitch" - /> - { const checkIndexPatternExists = async () => { try { - const dv = (await dataViews.find(indexName)).find(({ title }) => title === indexName); + const dv = (await dataViews.getIdsWithTitle()).find(({ title }) => title === indexName); if (dv !== undefined) { setIndexPatternExists(true); } else { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 88b0774e107e2..94a12f9ad5235 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -47,6 +47,49 @@ export interface CreateAnalyticsStepProps extends CreateAnalyticsFormProps { stepActivated?: boolean; } +async function checkIndexExists(destinationIndex: string) { + let resp; + let errorMessage; + try { + resp = await ml.checkIndicesExists({ indices: [destinationIndex] }); + } catch (e) { + errorMessage = extractErrorMessage(e); + } + return { resp, errorMessage }; +} + +async function retryIndexExistsCheck( + destinationIndex: string +): Promise<{ success: boolean; indexExists: boolean; errorMessage?: string }> { + let retryCount = 15; + + let resp = await checkIndexExists(destinationIndex); + let indexExists = resp.resp && resp.resp[destinationIndex] && resp.resp[destinationIndex].exists; + + while (retryCount > 1 && !indexExists) { + retryCount--; + await delay(1000); + resp = await checkIndexExists(destinationIndex); + indexExists = resp.resp && resp.resp[destinationIndex] && resp.resp[destinationIndex].exists; + } + + if (indexExists) { + return { success: true, indexExists: true }; + } + + return { + success: false, + indexExists: false, + ...(resp.errorMessage !== undefined ? { errorMessage: resp.errorMessage } : {}), + }; +} + +function delay(ms = 1000) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const mlContext = useMlContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); @@ -125,49 +168,88 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const createKibanaIndexPattern = async () => { const dataViewName = destinationIndex; - - try { - await mlContext.dataViewsContract.createAndSave( - { - title: dataViewName, - }, - false, - true - ); - - addRequestMessage({ - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.createDataViewSuccessMessage', - { - defaultMessage: 'Kibana data view {dataViewName} created.', - values: { dataViewName }, + const exists = await retryIndexExistsCheck(destinationIndex); + if (exists?.success === true) { + // index exists - create data view + if (exists?.indexExists === true) { + try { + await mlContext.dataViewsContract.createAndSave( + { + title: dataViewName, + }, + false, + true + ); + addRequestMessage({ + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.createDataViewSuccessMessage', + { + defaultMessage: 'Kibana data view {dataViewName} created.', + values: { dataViewName }, + } + ), + }); + } catch (e) { + // handle data view creation error + if (e instanceof DuplicateDataViewError) { + addRequestMessage({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessageError', + { + defaultMessage: 'The data view {dataViewName} already exists.', + values: { dataViewName }, + } + ), + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessage', + { + defaultMessage: 'An error occurred creating the Kibana data view:', + } + ), + }); + } else { + addRequestMessage({ + error: extractErrorMessage(e), + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.createDataViewErrorMessage', + { + defaultMessage: 'An error occurred creating the Kibana data view:', + } + ), + }); } - ), - }); - } catch (e) { - if (e instanceof DuplicateDataViewError) { + } + } + } else { + // Ran out of retries or there was a problem checking index exists + if (exists?.errorMessage) { addRequestMessage({ error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessageError', + 'xpack.ml.dataframe.analytics.create.errorCheckingDestinationIndexDataFrameAnalyticsJob', { - defaultMessage: 'The data view {dataViewName} already exists.', - values: { dataViewName }, + defaultMessage: '{errorMessage}', + values: { errorMessage: exists.errorMessage }, } ), message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessage', + 'xpack.ml.dataframe.analytics.create.errorOccurredCheckingDestinationIndexDataFrameAnalyticsJob', { - defaultMessage: 'An error occurred creating the Kibana data view:', + defaultMessage: 'An error occurred checking destination index exists.', } ), }); } else { addRequestMessage({ - error: extractErrorMessage(e), + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.destinationIndexNotCreatedForDataFrameAnalyticsJob', + { + defaultMessage: 'Destination index has not yet been created.', + } + ), message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.createDataViewErrorMessage', + 'xpack.ml.dataframe.analytics.create.unableToCreateDataViewForDataFrameAnalyticsJob', { - defaultMessage: 'An error occurred creating the Kibana data view:', + defaultMessage: 'Unable to create data view.', } ), }); diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index e1a839b21f7b0..29edb6106a993 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -118,6 +118,7 @@ "MlInfo", "MlEsSearch", "MlIndexExists", + "MlSpecificIndexExists", "JobAuditMessages", "GetJobAuditMessages", diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 726d4d080ec19..c0fb32df0fd18 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -225,17 +225,16 @@ export function systemRoutes( try { const { indices } = request.body; - const options = { - index: indices, - fields: ['*'], - ignore_unavailable: true, - allow_no_indices: true, - }; - - const { body } = await client.asCurrentUser.fieldCaps(options); + const results = await Promise.all( + indices.map(async (index) => + client.asCurrentUser.indices.exists({ + index, + }) + ) + ); - const result = indices.reduce((acc, cur) => { - acc[cur] = { exists: body.indices.includes(cur) }; + const result = indices.reduce((acc, cur, i) => { + acc[cur] = { exists: results[i].body }; return acc; }, {} as Record); diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 3f03680e687b1..242372586e7d3 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -129,6 +129,7 @@ export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logs export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; export const INDEX_ALERTS = '.monitoring-alerts-6*,.monitoring-alerts-7*'; export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; +export const INDEX_PATTERN_ENTERPRISE_SEARCH = '.monitoring-ent-search-*'; // This is the unique token that exists in monitoring indices collected by metricbeat export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; @@ -158,6 +159,7 @@ export const CODE_PATH_LOGSTASH = 'logstash'; export const CODE_PATH_APM = 'apm'; export const CODE_PATH_LICENSE = 'license'; export const CODE_PATH_LOGS = 'logs'; +export const CODE_PATH_ENTERPRISE_SEARCH = 'enterprise_search'; /** * The header sent by telemetry service when hitting Elasticsearch to identify query source @@ -177,6 +179,12 @@ export const KIBANA_SYSTEM_ID = 'kibana'; */ export const BEATS_SYSTEM_ID = 'beats'; +/** + * The name of the Enterprise Search System ID used to publish and look up Enterprise Search stats through the Monitoring system. + * @type {string} + */ +export const ENTERPRISE_SEARCH_SYSTEM_ID = 'enterprise_search'; + /** * The name of the Apm System ID used to publish and look up Apm stats through the Monitoring system. * @type {string} diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index a01e1c383a8e6..3c3d728b74dd5 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -677,6 +677,9 @@ export interface ElasticsearchMetricbeatSource { }; }; }; + enterprisesearch?: { + cluster_uuid?: string; + }; } export type ElasticsearchSource = ElasticsearchLegacySource & ElasticsearchMetricbeatSource; diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_breadcrumbs.ts b/x-pack/plugins/monitoring/public/application/hooks/use_breadcrumbs.ts index 5deac417ad3f5..0bfd258e7e489 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_breadcrumbs.ts @@ -186,6 +186,16 @@ function getApmBreadcrumbs(mainInstance: any) { return breadcrumbs; } +// generate Enterprise Search breadcrumbs +function getEnterpriseSearchBreadcrumbs(mainInstance: any) { + const entSearchLabel = i18n.translate('xpack.monitoring.breadcrumbs.entSearchLabel', { + defaultMessage: 'Enterprise Search', + }); + const breadcrumbs = []; + breadcrumbs.push(createCrumb('#/enterprise_search', entSearchLabel)); + return breadcrumbs; +} + function buildBreadcrumbs(clusterName: string, mainInstance?: any | null) { const homeCrumb = i18n.translate('xpack.monitoring.breadcrumbs.clustersLabel', { defaultMessage: 'Clusters', @@ -212,6 +222,9 @@ function buildBreadcrumbs(clusterName: string, mainInstance?: any | null) { if (mainInstance?.inApm) { breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance)); } + if (mainInstance?.inEnterpriseSearch) { + breadcrumbs = breadcrumbs.concat(getEnterpriseSearchBreadcrumbs(mainInstance)); + } return breadcrumbs; } diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index f18201110eddc..60af11ff8a6a3 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -28,6 +28,7 @@ import { CODE_PATH_KIBANA, CODE_PATH_LOGSTASH, CODE_PATH_APM, + CODE_PATH_ENTERPRISE_SEARCH, } from '../../common/constants'; import { BeatsInstancePage } from './pages/beats/instance'; import { ApmOverviewPage, ApmInstancesPage, ApmInstancePage } from './pages/apm'; @@ -43,6 +44,7 @@ import { ElasticsearchMLJobsPage } from './pages/elasticsearch/ml_jobs_page'; import { ElasticsearchNodeAdvancedPage } from './pages/elasticsearch/node_advanced_page'; import { ElasticsearchCcrPage } from './pages/elasticsearch/ccr_page'; import { ElasticsearchCcrShardPage } from './pages/elasticsearch/ccr_shard_page'; +import { EntSearchOverviewPage } from './pages/enterprise_search/overview'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; import { HeaderActionMenuContext } from './contexts/header_action_menu_context'; @@ -312,6 +314,13 @@ const MonitoringApp: React.FC<{ fetchAllClusters={false} /> + + = (props) => { + return ; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/enterprise_search/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/enterprise_search/overview.tsx new file mode 100644 index 0000000000000..50266c90aceeb --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/enterprise_search/overview.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ComponentProps } from '../../route_init'; +import { EntSearchTemplate } from './ent_search_template'; +import { GlobalStateContext } from '../../contexts/global_state_context'; +import { useCharts } from '../../hooks/use_charts'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { EnterpriseSearchOverview } from '../../../components/enterprise_search/overview'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; + +export const EntSearchOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + const [data, setData] = useState(null); + + const title = i18n.translate('xpack.monitoring.entSearch.overview.routeTitle', { + defaultMessage: 'Enterprise Search - Overview', + }); + + const pageTitle = i18n.translate('xpack.monitoring.entSearch.overview.pageTitle', { + defaultMessage: 'Enterprise Search Overview', + }); + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { inEnterpriseSearch: true }); + } + }, [cluster, generateBreadcrumbs]); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/enterprise_search`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + return ( + +
+ {data && } +
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js new file mode 100644 index 0000000000000..1459ccb2ecac6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/enterprise_search_panel.js @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { get } from 'lodash'; +import { formatNumber } from '../../../lib/format_number'; +import { + BytesPercentageUsage, + ClusterItemContainer, + DisabledIfNoDataAndInSetupModeLink, +} from './helpers'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGrid, + EuiFlexItem, + EuiTitle, + EuiPanel, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiHorizontalRule, + EuiFlexGroup, +} from '@elastic/eui'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; + +export function EnterpriseSearchPanel(props) { + const { setupMode } = props; + const setupModeData = get(setupMode.data, 'enterprise_search'); + + return ( + + + + + +

+ + + +

+
+ + + + + + + + {props.stats.versions[0] || + i18n.translate( + 'xpack.monitoring.cluster.overview.entSearchPanel.versionNotAvailableDescription', + { + defaultMessage: 'N/A', + } + )} + + + + + + + {props.stats.appSearchEngines} + + + + + + + {props.stats.workplaceSearchOrgSources} + + + + + + + {props.stats.workplaceSearchPrivateSources} + + +
+
+ + + + + + +

+ +

+
+
+
+ + + + + + + + + + + + + + {formatNumber(props.stats.uptime, 'time_since')} + + +
+
+
+
+ ); +} diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js index 5c766a23558de..1106340b35f38 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js @@ -94,6 +94,7 @@ export function ClusterItemContainer(props) { logstash: 'logoLogstash', beats: 'logoBeats', apm: 'apmApp', + enterprise_search: 'logoEnterpriseSearch', }; const icon = iconMap[props.url]; diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js index aa6181ffb9c54..05d5cb22c4c1b 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js @@ -12,6 +12,7 @@ import { LogstashPanel } from './logstash_panel'; import { BeatsPanel } from './beats_panel'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; +import { EnterpriseSearchPanel } from './enterprise_search_panel'; import { FormattedMessage } from '@kbn/i18n-react'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; @@ -57,6 +58,12 @@ export function Overview(props) { + + ); diff --git a/x-pack/plugins/monitoring/public/components/enterprise_search/overview/index.ts b/x-pack/plugins/monitoring/public/components/enterprise_search/overview/index.ts new file mode 100644 index 0000000000000..ffea796a8904e --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/enterprise_search/overview/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { EnterpriseSearchOverview } from './overview'; diff --git a/x-pack/plugins/monitoring/public/components/enterprise_search/overview/overview.tsx b/x-pack/plugins/monitoring/public/components/enterprise_search/overview/overview.tsx new file mode 100644 index 0000000000000..04c4f1fa54e16 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/enterprise_search/overview/overview.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiScreenReaderOnly, + EuiPanel, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +// @ts-ignore +import { MonitoringTimeseriesContainer } from '../../chart'; +import { Status } from './status'; + +export const EnterpriseSearchOverview: React.FC = ({ metrics, stats, ...rest }) => { + const lowLevelUsageMetrics = [ + metrics.enterprise_search_heap, + metrics.enterprise_search_jvm_finalizer_queue, + metrics.enterprise_search_gc_time, + metrics.enterprise_search_gc_rate, + metrics.enterprise_search_threads, + metrics.enterprise_search_threads_rate, + ]; + + const networkMetrics = [ + metrics.enterprise_search_http_traffic, + metrics.enterprise_search_http_responses, + metrics.enterprise_search_http_connections_current, + metrics.enterprise_search_http_connections_rate, + ]; + + const appSearchUsageMetrics = [metrics.app_search_total_engines, metrics.crawler_workers]; + + const workplaceSearchUsageMetrics = [metrics.workplace_search_total_sources]; + + return ( + + + +

+ +

+
+ + + + + + + + + +

+ +

+
+ + + + {networkMetrics.map((metric, index) => ( + + + + + ))} + +
+ + + + + +

+ +

+
+ + + + {lowLevelUsageMetrics.map((metric, index) => ( + + + + + ))} + +
+ + + + + +

+ +

+
+ + {appSearchUsageMetrics.map((metric, index) => ( + + + + + ))} + +
+ + + + + +

+ +

+
+ + {workplaceSearchUsageMetrics.map((metric, index) => ( + + + + + ))} + +
+
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/components/enterprise_search/overview/status.tsx b/x-pack/plugins/monitoring/public/components/enterprise_search/overview/status.tsx new file mode 100644 index 0000000000000..9f49d08a82622 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/enterprise_search/overview/status.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { formatMetric } from '../../../lib/format_number'; +// @ts-ignore +import { SummaryStatus } from '../../summary_status'; + +// @ts-ignore +export function Status({ stats }) { + const metrics = [ + { + label: i18n.translate('xpack.monitoring.entSearch.overview.instances', { + defaultMessage: 'Instances', + }), + value: formatMetric(stats.totalInstances, 'int_commas'), + 'data-test-subj': 'totalInstances', + }, + { + label: i18n.translate('xpack.monitoring.entSearch.overview.appSearchEngines', { + defaultMessage: 'App Search Engines', + }), + value: formatMetric(stats.appSearchEngines, 'int_commas'), + 'data-test-subj': 'appSearchEngines', + }, + { + label: i18n.translate('xpack.monitoring.entSearch.overview.workplaceSearchOrgSources', { + defaultMessage: 'Org Content Sources', + }), + value: formatMetric(stats.workplaceSearchOrgSources, 'int_commas'), + 'data-test-subj': 'workplaceSearchOrgSources', + }, + { + label: i18n.translate('xpack.monitoring.entSearch.overview.workplaceSearchPrivateSources', { + defaultMessage: 'Private Content Sources', + }), + value: formatMetric(stats.workplaceSearchPrivateSources, 'int_commas'), + 'data-test-subj': 'workplaceSearchPrivateSources', + }, + ]; + + return ; +} diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__snapshots__/get_clusters_summary.test.js.snap b/x-pack/plugins/monitoring/server/lib/cluster/__snapshots__/get_clusters_summary.test.js.snap index 12964129b518d..c837758016ff2 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__snapshots__/get_clusters_summary.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/cluster/__snapshots__/get_clusters_summary.test.js.snap @@ -56,6 +56,7 @@ Array [ }, "logs": undefined, }, + "enterpriseSearch": undefined, "isCcrEnabled": undefined, "isPrimary": true, "isSupported": true, @@ -141,6 +142,7 @@ Array [ }, "logs": undefined, }, + "enterpriseSearch": undefined, "isCcrEnabled": undefined, "isPrimary": false, "isSupported": true, @@ -231,6 +233,7 @@ Array [ }, "logs": undefined, }, + "enterpriseSearch": undefined, "isCcrEnabled": undefined, "isPrimary": false, "isSupported": true, @@ -316,6 +319,7 @@ Array [ }, "logs": undefined, }, + "enterpriseSearch": undefined, "isCcrEnabled": undefined, "isPrimary": false, "isSupported": true, diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts index 9016f1916542b..6e28070cdbfaa 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.ts @@ -13,6 +13,7 @@ import { getClustersStats } from './get_clusters_stats'; import { flagSupportedClusters } from './flag_supported_clusters'; import { getMlJobsForCluster } from '../elasticsearch'; import { getKibanasForClusters } from '../kibana'; +import { getEnterpriseSearchForClusters } from '../enterprise_search'; import { getLogstashForClusters } from '../logstash'; import { getLogstashPipelineIds } from '../logstash/get_pipeline_ids'; import { getBeatsForClusters } from '../beats'; @@ -26,6 +27,7 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, + CODE_PATH_ENTERPRISE_SEARCH, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; @@ -56,6 +58,7 @@ export async function getClustersFromRequest( lsIndexPattern, beatsIndexPattern, apmIndexPattern, + enterpriseSearchIndexPattern, filebeatIndexPattern, } = indexPatterns; @@ -72,7 +75,12 @@ export async function getClustersFromRequest( } if (!clusterUuid && !isStandaloneCluster) { - const indexPatternsToCheckForNonClusters = [lsIndexPattern, beatsIndexPattern, apmIndexPattern]; + const indexPatternsToCheckForNonClusters = [ + lsIndexPattern, + beatsIndexPattern, + apmIndexPattern, + enterpriseSearchIndexPattern, + ]; if (await hasStandaloneClusters(req, indexPatternsToCheckForNonClusters)) { clusters.push(getStandaloneClusterDefinition()); @@ -234,6 +242,22 @@ export async function getClustersFromRequest( } }); + // add Enterprise Search data + const enterpriseSearchByCluster = isInCodePath(codePaths, [CODE_PATH_ENTERPRISE_SEARCH]) + ? await getEnterpriseSearchForClusters(req, enterpriseSearchIndexPattern, clusters) + : []; + enterpriseSearchByCluster.forEach((entSearch) => { + const clusterIndex = clusters.findIndex( + (cluster) => + get(cluster, 'elasticsearch.cluster.id', cluster.cluster_uuid) === entSearch.clusterUuid + ); + if (clusterIndex >= 0) { + Reflect.set(clusters[clusterIndex], 'enterpriseSearch', { + ...entSearch, + }); + } + }); + // check ccr configuration const isCcrEnabled = await checkCcrEnabled(req, esIndexPattern); diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts index e29c1ad5292f9..01a7a5c4d8a52 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_summary.ts @@ -39,6 +39,7 @@ export function getClustersSummary( ml, beats, apm, + enterpriseSearch, alerts, ccs, cluster_settings: clusterSettings, @@ -149,6 +150,7 @@ export function getClustersSummary( ccs, beats, apm, + enterpriseSearch, alerts, isPrimary: kibana ? (kibana as EnhancedKibana).uuids?.includes(kibanaUuid) : false, status: calculateOverallStatus([ diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts index ccfe380edec09..5f7e55cf94c5a 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_index_patterns.ts @@ -13,6 +13,7 @@ import { INDEX_PATTERN_LOGSTASH, INDEX_PATTERN_BEATS, INDEX_ALERTS, + INDEX_PATTERN_ENTERPRISE_SEARCH, } from '../../../common/constants'; export function getIndexPatterns( @@ -27,6 +28,11 @@ export function getIndexPatterns( const beatsIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs); const apmIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_BEATS, ccs); const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); + const enterpriseSearchIndexPattern = prefixIndexPattern( + config, + INDEX_PATTERN_ENTERPRISE_SEARCH, + ccs + ); const indexPatterns = { esIndexPattern, kbnIndexPattern, @@ -34,6 +40,7 @@ export function getIndexPatterns( beatsIndexPattern, apmIndexPattern, alertsIndex, + enterpriseSearchIndexPattern, ...Object.keys(additionalPatterns).reduce((accum, varName) => { return { ...accum, diff --git a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts index a8de5529d8ca6..a8a1117839dfe 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_metrics.ts @@ -14,12 +14,13 @@ import { LegacyRequest } from '../../types'; type Metric = string | { keys: string | string[]; name: string }; +// TODO: Switch to an options object argument here export async function getMetrics( req: LegacyRequest, indexPattern: string, metricSet: Metric[] = [], filters: Array> = [], - metricOptions = {}, + metricOptions: Record = {}, numOfBuckets: number = 0, groupBy: string | Record | null = null ) { diff --git a/x-pack/plugins/monitoring/server/lib/details/get_series.ts b/x-pack/plugins/monitoring/server/lib/details/get_series.ts index 906c2df29fee0..99652cbff3ffd 100644 --- a/x-pack/plugins/monitoring/server/lib/details/get_series.ts +++ b/x-pack/plugins/monitoring/server/lib/details/get_series.ts @@ -13,7 +13,11 @@ import { checkParam } from '../error_missing_required'; import { metrics } from '../metrics'; import { createQuery } from '../create_query'; import { formatTimestampToDuration } from '../../../common'; -import { NORMALIZED_DERIVATIVE_UNIT, CALCULATE_DURATION_UNTIL } from '../../../common/constants'; +import { + NORMALIZED_DERIVATIVE_UNIT, + CALCULATE_DURATION_UNTIL, + STANDALONE_CLUSTER_CLUSTER_UUID, +} from '../../../common/constants'; import { formatUTCTimestampForTimezone } from '../format_timezone'; type SeriesBucket = Bucket & { metric_mb_deriv?: { normalized_value: number } }; @@ -180,7 +184,9 @@ async function fetchSeries( start: adjustedMin, end: Number(max), metric, - clusterUuid: req.params.clusterUuid, + clusterUuid: metricOptions.skipClusterUuidFilter + ? STANDALONE_CLUSTER_CLUSTER_UUID + : req.params.clusterUuid, // TODO: Pass in the UUID as an explicit function parameter uuid: getUuid(req, metric), filters, diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts index 3426c7ac42643..099b32dc1e65d 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_metrics.ts @@ -24,7 +24,7 @@ function calcSlope(data: Array<{ x: number; y: number }>) { const xySum = data.reduce((prev, curr) => prev + curr.y * curr.x, 0); const xSqSum = data.reduce((prev, curr) => prev + curr.x * curr.x, 0); const numerator = length * xySum - xSum * ySum; - const denominator = length * xSqSum - xSum * ySum; + const denominator = length * xSqSum - xSum * xSum; const slope = numerator / denominator; if (slope) { diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/_enterprise_search_stats.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/_enterprise_search_stats.ts new file mode 100644 index 0000000000000..77ee431cf5aa2 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/_enterprise_search_stats.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchResponse } from '../../../common/types/es'; + +export const entSearchAggFilterPath = [ + 'aggregations.total', + 'aggregations.versions.buckets', + + // Latest values + 'aggregations.app_search_engines.value', + 'aggregations.workplace_search_org_sources.value', + 'aggregations.workplace_search_private_sources.value', + + // Cluster-wide values + 'aggregations.uptime.value', + 'aggregations.cluster_heap_used.value', + 'aggregations.cluster_heap_total.value', + 'aggregations.cluster_heap_committed.value', +]; + +export const entSearchUuidsAgg = (maxBucketSize?: string) => ({ + // Count all unique agents + total: { + cardinality: { + field: 'agent.id', + precision_threshold: 10000, + }, + }, + + // Collect all runnng versions + versions: { + terms: { + field: 'enterprisesearch.health.version.number', + }, + }, + + // Get latest values for some of the metrics across the recent events + latest_report: { + terms: { + field: '@timestamp', + size: 2, // There is a health and a stats event and we want to make sure we always get at least one of each + order: { + _key: 'desc', + }, + }, + aggs: { + app_search_engines: { + max: { + field: 'enterprisesearch.stats.product_usage.app_search.total_engines', + }, + }, + workplace_search_org_sources: { + max: { + field: 'enterprisesearch.stats.product_usage.workplace_search.total_org_sources', + }, + }, + workplace_search_private_sources: { + max: { + field: 'enterprisesearch.stats.product_usage.workplace_search.total_private_sources', + }, + }, + }, + }, + + // Get per-instance values using ephemeral IDs to aggreagte metrics + ephemeral_ids: { + terms: { + field: 'agent.ephemeral_id', + size: maxBucketSize, + }, + aggs: { + uptime_max: { + max: { + field: 'enterprisesearch.health.process.uptime.sec', + }, + }, + heap_used: { + max: { + field: 'enterprisesearch.health.jvm.memory_usage.heap_used.bytes', + }, + }, + heap_total: { + max: { + field: 'enterprisesearch.health.jvm.memory_usage.heap_max.bytes', + }, + }, + heap_committed: { + max: { + field: 'enterprisesearch.health.jvm.memory_usage.heap_committed.bytes', + }, + }, + }, + }, + + // Get latest values from aggregations into global values + app_search_engines: { + max_bucket: { + buckets_path: 'latest_report>app_search_engines', + }, + }, + workplace_search_org_sources: { + max_bucket: { + buckets_path: 'latest_report>workplace_search_org_sources', + }, + }, + workplace_search_private_sources: { + max_bucket: { + buckets_path: 'latest_report>workplace_search_private_sources', + }, + }, + + // Aggregate metrics into global values + uptime: { + max_bucket: { + buckets_path: 'ephemeral_ids>uptime_max', + }, + }, + cluster_heap_used: { + sum_bucket: { + buckets_path: 'ephemeral_ids>heap_used', + }, + }, + cluster_heap_total: { + sum_bucket: { + buckets_path: 'ephemeral_ids>heap_total', + }, + }, + cluster_heap_committed: { + sum_bucket: { + buckets_path: 'ephemeral_ids>heap_committed', + }, + }, +}); + +export const entSearchAggResponseHandler = (response: ElasticsearchResponse) => { + const aggs = response.aggregations; + + // console.log('Aggs: ', aggs); + + const appSearchEngines = aggs?.app_search_engines.value ?? 0; + const workplaceSearchOrgSources = aggs?.workplace_search_org_sources.value ?? 0; + const workplaceSearchPrivateSources = aggs?.workplace_search_private_sources.value ?? 0; + + const totalInstances = aggs?.total.value ?? 0; + const uptime = aggs?.uptime.value ?? 0; + + const memUsed = aggs?.cluster_heap_used.value ?? 0; + const memCommitted = aggs?.cluster_heap_committed.value ?? 0; + const memTotal = aggs?.cluster_heap_total.value ?? 0; + + const versions = (aggs?.versions.buckets ?? []).map(({ key }: { key: string }) => key); + + return { + appSearchEngines, + workplaceSearchOrgSources, + workplaceSearchPrivateSources, + totalInstances, + uptime, + memUsed, + memCommitted, + memTotal, + versions, + }; +}; diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts new file mode 100644 index 0000000000000..3a418e010e1f7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/create_enterprise_search_query.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EnterpriseSearchMetric, EnterpriseSearchMetricFields } from '../metrics'; +import { createQuery } from '../create_query'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; + +/** + * {@code createQuery} for all Enterprise Search instances. + * + * @param {Object} options The options to pass to {@code createQuery} + */ +export function createEnterpriseSearchQuery(options: { + filters?: any[]; + types?: string[]; + metric?: EnterpriseSearchMetricFields; + uuid?: string; + start?: number; + end?: number; +}) { + const opts = { + filters: [] as any[], + metric: EnterpriseSearchMetric.getMetricFields(), + types: ['health', 'stats'], + clusterUuid: STANDALONE_CLUSTER_CLUSTER_UUID, // This is to disable the stack monitoring clusterUuid filter + ...(options ?? {}), + }; + + opts.filters.push({ + bool: { + should: [ + { term: { 'event.dataset': 'enterprisesearch.health' } }, + { term: { 'event.dataset': 'enterprisesearch.stats' } }, + ], + }, + }); + + return createQuery(opts); +} diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts new file mode 100644 index 0000000000000..96ba1d18dc9e8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_enterprise_search_for_clusters.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchResponse } from '../../../common/types/es'; +import { LegacyRequest, Cluster } from '../../types'; +import { checkParam } from '../error_missing_required'; +import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; +import { EnterpriseSearchMetric } from '../metrics'; +import { + entSearchAggFilterPath, + entSearchAggResponseHandler, + entSearchUuidsAgg, +} from './_enterprise_search_stats'; + +function handleResponse(clusterUuid: string, response: ElasticsearchResponse) { + const stats = entSearchAggResponseHandler(response); + + return { + clusterUuid, + stats, + }; +} + +export function getEnterpriseSearchForClusters( + req: LegacyRequest, + entSearchIndexPattern: string, + clusters: Cluster[] +) { + checkParam( + entSearchIndexPattern, + 'entSearchIndexPattern in enterprise_earch/getEnterpriseSearchForClusters' + ); + + const start = req.payload.timeRange.min; + const end = req.payload.timeRange.max; + const config = req.server.config(); + const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); + + return Promise.all( + clusters.map(async (cluster) => { + const clusterUuid = cluster.elasticsearch?.cluster?.id ?? cluster.cluster_uuid; + const params = { + index: entSearchIndexPattern, + size: 0, + ignore_unavailable: true, + filter_path: entSearchAggFilterPath, + body: { + query: createEnterpriseSearchQuery({ + start, + end, + uuid: clusterUuid, + metric: EnterpriseSearchMetric.getMetricFields(), + }), + aggs: entSearchUuidsAgg(maxBucketSize!), + }, + }; + + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const response = await callWithRequest(req, 'search', params); + return handleResponse(clusterUuid, response); + }) + ); +} diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts new file mode 100644 index 0000000000000..bcb5617e0c9d8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/get_stats.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { ElasticsearchResponse } from '../../../common/types/es'; +import { LegacyRequest } from '../../types'; +import { checkParam } from '../error_missing_required'; +import { createEnterpriseSearchQuery } from './create_enterprise_search_query'; +import { + entSearchAggFilterPath, + entSearchUuidsAgg, + entSearchAggResponseHandler, +} from './_enterprise_search_stats'; + +export async function getStats( + req: LegacyRequest, + entSearchIndexPattern: string, + clusterUuid: string +) { + checkParam(entSearchIndexPattern, 'entSearchIndexPattern in getStats'); + + const config = req.server.config(); + const start = moment.utc(req.payload.timeRange.min).valueOf(); + const end = moment.utc(req.payload.timeRange.max).valueOf(); + const maxBucketSize = config.get('monitoring.ui.max_bucket_size'); + + const params = { + index: entSearchIndexPattern, + filter_path: entSearchAggFilterPath, + size: 0, + ignore_unavailable: true, + body: { + query: createEnterpriseSearchQuery({ + start, + end, + uuid: clusterUuid, + }), + aggs: entSearchUuidsAgg(maxBucketSize!), + }, + }; + + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const response: ElasticsearchResponse = await callWithRequest(req, 'search', params); + + return entSearchAggResponseHandler(response); +} diff --git a/x-pack/plugins/monitoring/server/lib/enterprise_search/index.ts b/x-pack/plugins/monitoring/server/lib/enterprise_search/index.ts new file mode 100644 index 0000000000000..4eba42331a789 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/enterprise_search/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getEnterpriseSearchForClusters } from './get_enterprise_search_for_clusters'; +export { getStats } from './get_stats'; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap index cbd6a610d5eb8..edb76085e6834 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/metrics/__snapshots__/metrics.test.js.snap @@ -1563,6 +1563,18 @@ Object { "units": "", "uuidField": "beats_stats.beat.uuid", }, + "app_search_total_engines": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Current number of App Search engines within the Enterprise Search deployment.", + "field": "enterprisesearch.stats.product_usage.app_search.total_engines", + "format": "0.[00]", + "label": "App Search Engines", + "metricAgg": "avg", + "timestampField": "@timestamp", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, "beat_bytes_written": BeatsByteRateMetric { "app": "beats", "derivative": true, @@ -2481,6 +2493,275 @@ Object { "units": "/s", "uuidField": "source_node.uuid", }, + "crawler_workers_active": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Currently active App Search crawler workers.", + "field": "enterprisesearch.health.crawler.workers.active", + "format": "0.[00]", + "label": "Active", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "crawler_workers_total": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "The number of crawler workers configured across all instances of App Search.", + "field": "enterprisesearch.health.crawler.workers.pool_size", + "format": "0.[00]", + "label": "Total", + "metricAgg": "max", + "timestampField": "@timestamp", + "title": "Crawler Workers", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_daemon_threads_current": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Currently running JVM daemon threads used by the application.", + "field": "enterprisesearch.health.jvm.threads.daemon", + "format": "0.[00]", + "label": "Daemon Threads", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_gc_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "The rate of JVM garbage collector invocations across the fleet.", + "field": "enterprisesearch.health.jvm.gc.collection_count", + "format": "0.[00]", + "label": "JVM GC Rate", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_gc_time": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Time spent performing JVM garbage collections.", + "field": "enterprisesearch.health.jvm.gc.collection_time.ms", + "format": "0,0.[00]", + "label": "Time spent on JVM garbage collection", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "ms", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_heap_committed": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "The amount of memory JVM has allocated from the OS and is available to the application.", + "field": "enterprisesearch.health.jvm.memory_usage.heap_committed.bytes", + "format": "0,0.0 b", + "label": "Committed", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "bytes", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_heap_total": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Maximum amount of JVM heap memory available to the application.", + "field": "enterprisesearch.health.jvm.memory_usage.heap_max.bytes", + "format": "0,0.0 b", + "label": "Total", + "metricAgg": "max", + "timestampField": "@timestamp", + "title": "JVM Heap Usage", + "units": "bytes", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_heap_used": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Current amount of JVM Heam memory used by the application.", + "field": "enterprisesearch.health.jvm.memory_usage.heap_used.bytes", + "format": "0,0.0 b", + "label": "Used", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "bytes", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_1xx_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Outgoing HTTP 1xx responses across all instances in the deployment.", + "field": "enterprisesearch.stats.http.responses.1xx", + "format": "0,0.[00]", + "label": "1xx", + "metricAgg": "max", + "timestampField": "@timestamp", + "title": "HTTP Responses", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_2xx_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Outgoing HTTP 2xx responses across all instances in the deployment.", + "field": "enterprisesearch.stats.http.responses.2xx", + "format": "0,0.[00]", + "label": "2xx", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_3xx_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Outgoing HTTP 3xx responses across all instances in the deployment.", + "field": "enterprisesearch.stats.http.responses.3xx", + "format": "0,0.[00]", + "label": "3xx", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_4xx_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Outgoing HTTP 4xx responses across all instances in the deployment.", + "field": "enterprisesearch.stats.http.responses.4xx", + "format": "0,0.[00]", + "label": "4xx", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_5xx_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Outgoing HTTP 5xx responses across all instances in the deployment.", + "field": "enterprisesearch.stats.http.responses.5xx", + "format": "0,0.[00]", + "label": "5xx", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_bytes_received_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Incoming HTTP traffic rate across all instances in the deployment.", + "field": "enterprisesearch.stats.http.network.received.bytes", + "format": "0,0.0 b", + "label": "Received", + "metricAgg": "max", + "timestampField": "@timestamp", + "title": "HTTP Traffic", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_bytes_received_total": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Total number of bytes received by all instances in the deployment.", + "field": "enterprisesearch.stats.http.network.received.bytes", + "format": "0,0.0 b", + "label": "HTTP Bytes Received", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "bytes", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_bytes_sent_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Outgoing HTTP traffic across all instances in the deployment.", + "field": "enterprisesearch.stats.http.network.sent.bytes", + "format": "0,0.0 b", + "label": "Sent", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_bytes_sent_total": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Total number of bytes sent by all instances in the deployment.", + "field": "enterprisesearch.stats.http.network.sent.bytes", + "format": "0,0.0 b", + "label": "HTTP Bytes Sent", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "bytes", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_connections_current": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Currently open incoming HTTP connections across all instances.", + "field": "enterprisesearch.stats.http.connections.current", + "format": "0.[00]", + "label": "Open HTTP Connections", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_http_connections_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "The rate of incoming HTTP connections across all instances.", + "field": "enterprisesearch.stats.http.connections.total", + "format": "0,0.[00]", + "label": "HTTP Connections Rate", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_jvm_finalizer_queue": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Number of objects within the JVM heap waiting for the finalizer thread.", + "field": "enterprisesearch.health.jvm.memory_usage.object_pending_finalization_count", + "format": "0.[00]", + "label": "JVM Objects Pending Finalization", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_threads_current": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Currently running JVM threads used by the application.", + "field": "enterprisesearch.health.jvm.threads.current", + "format": "0.[00]", + "label": "Active Threads", + "metricAgg": "max", + "timestampField": "@timestamp", + "title": "JVM Threads", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "enterprise_search_threads_rate": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": true, + "description": "Currently running JVM threads used by the application.", + "field": "enterprisesearch.health.jvm.threads.total_started", + "format": "0.[00]", + "label": "Thread Creation Rate", + "metricAgg": "max", + "timestampField": "@timestamp", + "units": "/s", + "uuidField": "enterprisesearch.cluster_uuid", + }, "index_document_count": ElasticsearchMetric { "app": "elasticsearch", "derivative": false, @@ -4709,5 +4990,30 @@ Object { "units": "/s", "uuidField": "source_node.uuid", }, + "workplace_search_total_org_sources": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Current number of Workplace Search org-wide content sources within the Enterprise Search deployment.", + "field": "enterprisesearch.stats.product_usage.workplace_search.total_org_sources", + "format": "0.[00]", + "label": "Org Sources", + "metricAgg": "avg", + "timestampField": "@timestamp", + "title": "Workpace Search Content Sources", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, + "workplace_search_total_private_sources": EnterpriseSearchMetric { + "app": "enterprise_search", + "derivative": false, + "description": "Current number of Workplace Search private content sources within the Enterprise Search deployment.", + "field": "enterprisesearch.stats.product_usage.workplace_search.total_private_sources", + "format": "0.[00]", + "label": "Private Sources", + "metricAgg": "avg", + "timestampField": "@timestamp", + "units": "", + "uuidField": "enterprisesearch.cluster_uuid", + }, } `; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/enterprise_search/classes.ts b/x-pack/plugins/monitoring/server/lib/metrics/enterprise_search/classes.ts new file mode 100644 index 0000000000000..b1e2ab691c0e7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/metrics/enterprise_search/classes.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// @ts-ignore +import { Metric } from '../classes'; + +export class EnterpriseSearchMetric extends Metric { + // @ts-ignore + constructor(opts) { + super({ + ...opts, + app: 'enterprise_search', + ...EnterpriseSearchMetric.getMetricFields(), + }); + } + + static getMetricFields() { + return { + uuidField: 'enterprisesearch.cluster_uuid', + timestampField: '@timestamp', + }; + } +} + +export type EnterpriseSearchMetricFields = ReturnType< + typeof EnterpriseSearchMetric.getMetricFields +>; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/enterprise_search/metrics.js b/x-pack/plugins/monitoring/server/lib/metrics/enterprise_search/metrics.js new file mode 100644 index 0000000000000..39a8475471d12 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/metrics/enterprise_search/metrics.js @@ -0,0 +1,408 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EnterpriseSearchMetric } from './classes'; +import { LARGE_BYTES, SMALL_FLOAT, LARGE_FLOAT } from '../../../../common/formatting'; +import { i18n } from '@kbn/i18n'; + +const perSecondUnitLabel = i18n.translate('xpack.monitoring.metrics.entSearch.perSecondUnitLabel', { + defaultMessage: '/s', +}); + +const msTimeUnitLabel = i18n.translate('xpack.monitoring.metrics.entSearch.msTimeUnitLabel', { + defaultMessage: 'ms', +}); + +export const metrics = { + app_search_total_engines: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.product_usage.app_search.total_engines', + metricAgg: 'avg', + label: i18n.translate('xpack.monitoring.metrics.entSearch.app_search_engines', { + defaultMessage: 'App Search Engines', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.app_search_engines.description', + { + defaultMessage: + 'Current number of App Search engines within the Enterprise Search deployment.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + workplace_search_total_org_sources: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.product_usage.workplace_search.total_org_sources', + metricAgg: 'avg', + title: i18n.translate('xpack.monitoring.metrics.entSearch.workplace_search_content_sources', { + defaultMessage: 'Workpace Search Content Sources', + }), + label: i18n.translate('xpack.monitoring.metrics.entSearch.workplace_search_org_sources', { + defaultMessage: 'Org Sources', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.workplace_search_org_sources.description', + { + defaultMessage: + 'Current number of Workplace Search org-wide content sources within the Enterprise Search deployment.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + workplace_search_total_private_sources: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.product_usage.workplace_search.total_private_sources', + metricAgg: 'avg', + label: i18n.translate('xpack.monitoring.metrics.entSearch.workplace_search_private_sources', { + defaultMessage: 'Private Sources', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.workplace_search_private_sources.description', + { + defaultMessage: + 'Current number of Workplace Search private content sources within the Enterprise Search deployment.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + enterprise_search_heap_total: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.memory_usage.heap_max.bytes', + metricAgg: 'max', + title: i18n.translate('xpack.monitoring.metrics.entSearch.jvm_heap_usage', { + defaultMessage: 'JVM Heap Usage', + }), + label: i18n.translate('xpack.monitoring.metrics.entSearch.heap_total', { + defaultMessage: 'Total', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.enterpriseSearch.heap_total.description', + { + defaultMessage: 'Maximum amount of JVM heap memory available to the application.', + } + ), + format: LARGE_BYTES, + units: 'bytes', + }), + + enterprise_search_heap_committed: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.memory_usage.heap_committed.bytes', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.heap_committed', { + defaultMessage: 'Committed', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.heap_committed.description', { + defaultMessage: + 'The amount of memory JVM has allocated from the OS and is available to the application.', + }), + format: LARGE_BYTES, + units: 'bytes', + }), + + enterprise_search_heap_used: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.memory_usage.heap_used.bytes', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.heap_used', { + defaultMessage: 'Used', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.heap_used.description', { + defaultMessage: 'Current amount of JVM Heam memory used by the application.', + }), + format: LARGE_BYTES, + units: 'bytes', + }), + + enterprise_search_gc_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.gc.collection_count', + derivative: true, + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.gc_rate', { + defaultMessage: 'JVM GC Rate', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.gc_rate.description', { + defaultMessage: 'The rate of JVM garbage collector invocations across the fleet.', + }), + format: SMALL_FLOAT, + units: perSecondUnitLabel, + }), + + enterprise_search_gc_time: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.gc.collection_time.ms', + derivative: true, + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.gc_time', { + defaultMessage: 'Time spent on JVM garbage collection', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.gc_time.description', { + defaultMessage: 'Time spent performing JVM garbage collections.', + }), + format: LARGE_FLOAT, + units: msTimeUnitLabel, + }), + + enterprise_search_threads_current: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.threads.current', + metricAgg: 'max', + title: i18n.translate('xpack.monitoring.metrics.entSearch.threads', { + defaultMessage: 'JVM Threads', + }), + label: i18n.translate('xpack.monitoring.metrics.entSearch.threads.current', { + defaultMessage: 'Active Threads', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.threads.current.description', { + defaultMessage: 'Currently running JVM threads used by the application.', + }), + format: SMALL_FLOAT, + units: '', + }), + + enterprise_search_daemon_threads_current: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.threads.daemon', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.threads.daemon', { + defaultMessage: 'Daemon Threads', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.threads.daemon.description', { + defaultMessage: 'Currently running JVM daemon threads used by the application.', + }), + format: SMALL_FLOAT, + units: '', + }), + + enterprise_search_jvm_finalizer_queue: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.memory_usage.object_pending_finalization_count', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.finalizer_objects', { + defaultMessage: 'JVM Objects Pending Finalization', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.finalizer_objects.description', + { + defaultMessage: 'Number of objects within the JVM heap waiting for the finalizer thread.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + enterprise_search_threads_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.jvm.threads.total_started', + metricAgg: 'max', + derivative: true, + label: i18n.translate('xpack.monitoring.metrics.entSearch.threads.rate', { + defaultMessage: 'Thread Creation Rate', + }), + description: i18n.translate('xpack.monitoring.metrics.entSearch.threads.rate.description', { + defaultMessage: 'Currently running JVM threads used by the application.', + }), + format: SMALL_FLOAT, + units: perSecondUnitLabel, + }), + + crawler_workers_total: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.crawler.workers.pool_size', + metricAgg: 'max', + title: i18n.translate('xpack.monitoring.metrics.entSearch.crawler_workers', { + defaultMessage: 'Crawler Workers', + }), + label: i18n.translate('xpack.monitoring.metrics.entSearch.total_crawler_workers', { + defaultMessage: 'Total', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.total_crawler_workers.description', + { + defaultMessage: + 'The number of crawler workers configured across all instances of App Search.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + crawler_workers_active: new EnterpriseSearchMetric({ + field: 'enterprisesearch.health.crawler.workers.active', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.active_crawler_workers', { + defaultMessage: 'Active', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.active_crawler_workers.description', + { + defaultMessage: 'Currently active App Search crawler workers.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + enterprise_search_http_connections_current: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.connections.current', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.http_connections.current', { + defaultMessage: 'Open HTTP Connections', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.http_connections.current.description', + { + defaultMessage: 'Currently open incoming HTTP connections across all instances.', + } + ), + format: SMALL_FLOAT, + units: '', + }), + + enterprise_search_http_connections_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.connections.total', + metricAgg: 'max', + derivative: true, + label: i18n.translate('xpack.monitoring.metrics.entSearch.http_connections.rate', { + defaultMessage: 'HTTP Connections Rate', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.current_http_connections.description', + { + defaultMessage: 'The rate of incoming HTTP connections across all instances.', + } + ), + format: LARGE_FLOAT, + units: perSecondUnitLabel, + }), + + enterprise_search_http_bytes_received_total: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.network.received.bytes', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.http_bytes_received.total', { + defaultMessage: 'HTTP Bytes Received', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.http_bytes_received.total.description', + { + defaultMessage: 'Total number of bytes received by all instances in the deployment.', + } + ), + format: LARGE_BYTES, + units: 'bytes', + }), + + enterprise_search_http_bytes_received_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.network.received.bytes', + metricAgg: 'max', + derivative: true, + title: i18n.translate('xpack.monitoring.metrics.entSearch.http_traffic', { + defaultMessage: 'HTTP Traffic', + }), + label: i18n.translate('xpack.monitoring.metrics.entSearch.http_bytes_received.rate', { + defaultMessage: 'Received', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.http_bytes_received.rate.description', + { + defaultMessage: 'Incoming HTTP traffic rate across all instances in the deployment.', + } + ), + format: LARGE_BYTES, + units: perSecondUnitLabel, + }), + + enterprise_search_http_bytes_sent_total: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.network.sent.bytes', + metricAgg: 'max', + label: i18n.translate('xpack.monitoring.metrics.entSearch.http_bytes_sent.total', { + defaultMessage: 'HTTP Bytes Sent', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.http_bytes_sent.total.description', + { + defaultMessage: 'Total number of bytes sent by all instances in the deployment.', + } + ), + format: LARGE_BYTES, + units: 'bytes', + }), + + enterprise_search_http_bytes_sent_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.network.sent.bytes', + metricAgg: 'max', + derivative: true, + label: i18n.translate('xpack.monitoring.metrics.entSearch.http_bytes_sent.rate', { + defaultMessage: 'Sent', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.entSearch.http_bytes_sent.rate.description', + { + defaultMessage: 'Outgoing HTTP traffic across all instances in the deployment.', + } + ), + format: LARGE_BYTES, + units: perSecondUnitLabel, + }), + + enterprise_search_http_1xx_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.responses.1xx', + metricAgg: 'max', + derivative: true, + title: i18n.translate('xpack.monitoring.metrics.entSearch.http_response_rate', { + defaultMessage: 'HTTP Responses', + }), + label: '1xx', + description: i18n.translate('xpack.monitoring.metrics.entSearch.http_1xx.rate.description', { + defaultMessage: 'Outgoing HTTP 1xx responses across all instances in the deployment.', + }), + format: LARGE_FLOAT, + units: perSecondUnitLabel, + }), + + enterprise_search_http_2xx_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.responses.2xx', + metricAgg: 'max', + derivative: true, + label: '2xx', + description: i18n.translate('xpack.monitoring.metrics.entSearch.http_2xx.rate.description', { + defaultMessage: 'Outgoing HTTP 2xx responses across all instances in the deployment.', + }), + format: LARGE_FLOAT, + units: perSecondUnitLabel, + }), + + enterprise_search_http_3xx_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.responses.3xx', + metricAgg: 'max', + derivative: true, + label: '3xx', + description: i18n.translate('xpack.monitoring.metrics.entSearch.http_3xx.rate.description', { + defaultMessage: 'Outgoing HTTP 3xx responses across all instances in the deployment.', + }), + format: LARGE_FLOAT, + units: perSecondUnitLabel, + }), + + enterprise_search_http_4xx_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.responses.4xx', + metricAgg: 'max', + derivative: true, + label: '4xx', + description: i18n.translate('xpack.monitoring.metrics.entSearch.http_4xx.rate.description', { + defaultMessage: 'Outgoing HTTP 4xx responses across all instances in the deployment.', + }), + format: LARGE_FLOAT, + units: perSecondUnitLabel, + }), + + enterprise_search_http_5xx_rate: new EnterpriseSearchMetric({ + field: 'enterprisesearch.stats.http.responses.5xx', + metricAgg: 'max', + derivative: true, + label: '5xx', + description: i18n.translate('xpack.monitoring.metrics.entSearch.http_5xx.rate.description', { + defaultMessage: 'Outgoing HTTP 5xx responses across all instances in the deployment.', + }), + format: LARGE_FLOAT, + units: perSecondUnitLabel, + }), +}; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/index.ts b/x-pack/plugins/monitoring/server/lib/metrics/index.ts index 2fdaeb81ee620..50f477c27f49a 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/index.ts +++ b/x-pack/plugins/monitoring/server/lib/metrics/index.ts @@ -15,5 +15,7 @@ export { ApmMetric, ApmClusterMetric } from './apm/classes'; export { LogstashClusterMetric, LogstashMetric } from './logstash/classes'; export type { BeatsMetricFields } from './beats/classes'; export { BeatsClusterMetric, BeatsMetric } from './beats/classes'; +export { EnterpriseSearchMetric } from './enterprise_search/classes'; +export type { EnterpriseSearchMetricFields } from './enterprise_search/classes'; // @ts-ignore export { metrics } from './metrics'; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/metrics.js b/x-pack/plugins/monitoring/server/lib/metrics/metrics.js index 972643e750e0e..58cc61f6bbdc5 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/metrics.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/metrics.js @@ -10,6 +10,7 @@ import { metrics as kibanaMetrics } from './kibana/metrics'; import { metrics as logstashMetrics } from './logstash/metrics'; import { metrics as beatsMetrics } from './beats/metrics'; import { metrics as apmMetrics } from './apm/metrics'; +import { metrics as entSearchMetrics } from './enterprise_search/metrics'; export const metrics = { ...elasticsearchMetrics, @@ -17,4 +18,5 @@ export const metrics = { ...logstashMetrics, ...beatsMetrics, ...apmMetrics, + ...entSearchMetrics, }; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.js new file mode 100644 index 0000000000000..4eb6af5bb116d --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/index.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { entSearchOverviewRoute } from './overview'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.js new file mode 100644 index 0000000000000..4bec4bc3948c5 --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/metric_set_overview.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const metricSet = [ + // Low level usage metrics + { + name: 'enterprise_search_heap', + keys: [ + 'enterprise_search_heap_total', + 'enterprise_search_heap_committed', + 'enterprise_search_heap_used', + ], + }, + 'enterprise_search_jvm_finalizer_queue', + 'enterprise_search_gc_time', + 'enterprise_search_gc_rate', + { + name: 'enterprise_search_threads', + keys: ['enterprise_search_threads_current', 'enterprise_search_daemon_threads_current'], + }, + 'enterprise_search_threads_rate', + + // Networking metrics + 'enterprise_search_http_connections_current', + 'enterprise_search_http_connections_rate', + { + name: 'enterprise_search_http_traffic', + keys: ['enterprise_search_http_bytes_received_rate', 'enterprise_search_http_bytes_sent_rate'], + }, + { + name: 'enterprise_search_http_responses', + keys: [ + 'enterprise_search_http_1xx_rate', + 'enterprise_search_http_2xx_rate', + 'enterprise_search_http_3xx_rate', + 'enterprise_search_http_4xx_rate', + 'enterprise_search_http_5xx_rate', + ], + }, + + // App Search usage metrics + 'app_search_total_engines', + { + name: 'crawler_workers', + keys: ['crawler_workers_total', 'crawler_workers_active'], + }, + + // Workplace Search usage metrics + { + name: 'workplace_search_total_sources', + keys: ['workplace_search_total_org_sources', 'workplace_search_total_private_sources'], + }, +]; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js new file mode 100644 index 0000000000000..b9bc0f49bc99d --- /dev/null +++ b/x-pack/plugins/monitoring/server/routes/api/v1/enterprise_search/overview.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { prefixIndexPattern } from '../../../../../common/ccs_utils'; +import { getMetrics } from '../../../../lib/details/get_metrics'; +import { metricSet } from './metric_set_overview'; +import { handleError } from '../../../../lib/errors'; +import { INDEX_PATTERN_ENTERPRISE_SEARCH } from '../../../../../common/constants'; +import { getStats } from '../../../../lib/enterprise_search'; + +export function entSearchOverviewRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/clusters/{clusterUuid}/enterprise_search', + config: { + validate: { + params: schema.object({ + clusterUuid: schema.string(), + }), + payload: schema.object({ + ccs: schema.maybe(schema.string()), + timeRange: schema.object({ + min: schema.string(), + max: schema.string(), + }), + }), + }, + }, + + async handler(req) { + const clusterUuid = req.params.clusterUuid; + const entSearchIndexPattern = prefixIndexPattern( + server.config(), + INDEX_PATTERN_ENTERPRISE_SEARCH, + req.payload.ccs + ); + + try { + const [stats, metrics] = await Promise.all([ + getStats(req, entSearchIndexPattern, clusterUuid), + getMetrics(req, entSearchIndexPattern, metricSet, [], { + skipClusterUuidFilter: true, + }), + ]); + + return { stats, metrics }; + } catch (err) { + return handleError(err, req); + } + }, + }); +} diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/plugins/monitoring/server/routes/api/v1/ui.js index 498b4c0978270..9cc28d82d7dbb 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/ui.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/ui.js @@ -38,4 +38,5 @@ export { logstashPipelineRoute, logstashClusterPipelineIdsRoute, } from './logstash'; +export { entSearchOverviewRoute } from './enterprise_search'; export * from './setup'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 640e928b8ab98..074a9e1ca6780 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -112,7 +112,7 @@ const HIDE_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.hide' }); const COPY_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.clone', { - defaultMessage: 'Copy series', + defaultMessage: 'Duplicate series', }); const VIEW_SAMPLE_DOCUMENTS_LABEL = i18n.translate( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx index ccad461209313..cbd7efc42d964 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx @@ -7,10 +7,17 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { mockUxSeries, render } from '../../rtl_helpers'; import { SeriesName } from './series_name'; -describe.skip('SeriesChartTypesSelect', function () { +// ensures that fields appropriately match to their label +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('SeriesName', function () { it('should render properly', async function () { render(); @@ -20,7 +27,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should display input when editing name', async function () { render(); - let input = screen.queryByLabelText(mockUxSeries.name); + let input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; // read only expect(input).not.toBeInTheDocument(); @@ -30,17 +37,52 @@ describe.skip('SeriesChartTypesSelect', function () { fireEvent.click(editButton); await waitFor(() => { - input = screen.getByLabelText(mockUxSeries.name); + input = screen.getByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; expect(input).toBeInTheDocument(); + expect(input.value).toBe(mockUxSeries.name); }); // toggle readonly fireEvent.click(editButton); await waitFor(() => { - input = screen.getByLabelText(mockUxSeries.name); + input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + + expect(screen.getByText(mockUxSeries.name)).toBeInTheDocument(); + expect(input).not.toBeInTheDocument(); + }); + }); + + it('should save name on enter key', async function () { + const newName = '-test-new-name'; + render(); + + let input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + + // read only + expect(input).not.toBeInTheDocument(); + + const editButton = screen.getByRole('button'); + // toggle editing + userEvent.click(editButton); + + await waitFor(() => { + input = screen.getByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + + expect(input).toBeInTheDocument(); + }); + + userEvent.click(input); + userEvent.type(input, newName); + + // submit + userEvent.keyboard('{enter}'); + + await waitFor(() => { + input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + expect(screen.getByText(`${mockUxSeries.name}${newName}`)).toBeInTheDocument(); expect(input).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx index cff30a2b35059..68a628e23292c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, ChangeEvent, useEffect, useRef } from 'react'; +import React, { useState, ChangeEvent, useEffect, useRef, KeyboardEventHandler } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { @@ -57,6 +57,12 @@ export function SeriesName({ series, seriesId }: Props) { } }; + const onKeyDown: KeyboardEventHandler = (event) => { + if (event.key === 'Enter') { + setIsEditingEnabled(false); + } + }; + useEffect(() => { setValue(series.name); }, [series.name]); @@ -75,12 +81,14 @@ export function SeriesName({ series, seriesId }: Props) { diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 4b691c15d1b3c..7f3a3db42556e 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -32,4 +32,3 @@ export const config: PluginConfigDescriptor = { export type RuleRegistryPluginConfig = TypeOf; export const INDEX_PREFIX = '.alerts' as const; -export const INDEX_PREFIX_FOR_BACKING_INDICES = '.internal.alerts' as const; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts index 63a159121e009..3daf5cd722ce9 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.mock.ts @@ -29,6 +29,7 @@ export const createRuleDataClientMock = ( indexName, kibanaVersion: '7.16.0', isWriteEnabled: jest.fn(() => true), + indexNameWithNamespace: jest.fn((namespace: string) => indexName + namespace), // @ts-ignore 4.3.5 upgrade getReader: jest.fn((_options?: { namespace?: string }) => ({ diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts index d7ec6ea41ac8f..4aa0126cdabf8 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/rule_data_client.ts @@ -54,6 +54,10 @@ export class RuleDataClient implements IRuleDataClient { return this.options.indexInfo.kibanaVersion; } + public indexNameWithNamespace(namespace: string): string { + return this.options.indexInfo.getPrimaryAlias(namespace); + } + private get writeEnabled(): boolean { return this._isWriteEnabled; } @@ -192,7 +196,7 @@ export class RuleDataClient implements IRuleDataClient { return clusterClient.bulk(requestWithDefaultParameters).then((response) => { if (response.body.errors) { const error = new errors.ResponseError(response); - throw error; + this.options.logger.error(error); } return response; }); diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 5ddbd0035526d..5fab32eb38868 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -14,6 +14,7 @@ import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_fie export interface IRuleDataClient { indexName: string; + indexNameWithNamespace(namespace: string): string; kibanaVersion: string; isWriteEnabled(): boolean; getReader(options?: { namespace?: string }): IRuleDataReader; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts index 52fef63a732f0..eca44c550411f 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_info.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { INDEX_PREFIX, INDEX_PREFIX_FOR_BACKING_INDICES } from '../config'; +import { INDEX_PREFIX } from '../config'; import { IndexOptions } from './index_options'; import { joinWithDash } from './utils'; @@ -23,16 +23,16 @@ interface ConstructorOptions { export class IndexInfo { constructor(options: ConstructorOptions) { const { indexOptions, kibanaVersion } = options; - const { registrationContext, dataset } = indexOptions; + const { registrationContext, dataset, additionalPrefix } = indexOptions; this.indexOptions = indexOptions; this.kibanaVersion = kibanaVersion; - this.baseName = joinWithDash(INDEX_PREFIX, `${registrationContext}.${dataset}`); - this.basePattern = joinWithDash(this.baseName, '*'); - this.baseNameForBackingIndices = joinWithDash( - INDEX_PREFIX_FOR_BACKING_INDICES, + this.baseName = joinWithDash( + `${additionalPrefix ?? ''}${INDEX_PREFIX}`, `${registrationContext}.${dataset}` ); + this.basePattern = joinWithDash(this.baseName, '*'); + this.baseNameForBackingIndices = `.internal${this.baseName}`; } /** diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts index ba0961c7926a1..cdec7c609699d 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index_options.ts @@ -95,6 +95,17 @@ export interface IndexOptions { * @example '.siem-signals', undefined */ secondaryAlias?: string; + + /** + * Optional prefix name that will be prepended to indices in addition to + * primary dataset and context naming convention. + * + * Currently used only for creating a preview index for the purpose of + * previewing alerts from a rule. The documents are identical to alerts, but + * shouldn't exist on an alert index and shouldn't be queried together with + * real alerts in any way, because the rule that created them doesn't exist + */ + additionalPrefix?: string; } /** diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts index de1193771dd95..2d914e5e0945e 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts @@ -106,14 +106,16 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper } return { - createdAlerts: augmentedAlerts.map((alert, idx) => { - const responseItem = response.body.items[idx].create; - return { - _id: responseItem?._id ?? '', - _index: responseItem?._index ?? '', - ...alert._source, - }; - }), + createdAlerts: augmentedAlerts + .map((alert, idx) => { + const responseItem = response.body.items[idx].create; + return { + _id: responseItem?._id ?? '', + _index: responseItem?._index ?? '', + ...alert._source, + }; + }) + .filter((_, idx) => response.body.items[idx].create?.status === 201), }; } else { logger.debug('Writing is disabled.'); diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/lib/get_connection_count.ts b/x-pack/plugins/saved_objects_tagging/server/routes/lib/get_connection_count.ts index c20fa83738f9d..aa2e87a71fcbb 100644 --- a/x-pack/plugins/saved_objects_tagging/server/routes/lib/get_connection_count.ts +++ b/x-pack/plugins/saved_objects_tagging/server/routes/lib/get_connection_count.ts @@ -5,9 +5,13 @@ * 2.0. */ -import { SavedObjectsClientContract, SavedObjectsFindOptionsReference } from 'src/core/server'; +import { + SavedObjectsClientContract, + SavedObjectsFindOptionsReference, + SavedObject, +} from 'src/core/server'; import { tagSavedObjectTypeName } from '../../../common/constants'; -import { Tag, TagWithRelations } from '../../../common/types'; +import { Tag, TagAttributes, TagWithRelations } from '../../../common/types'; export const addConnectionCount = async ( tags: Tag[], @@ -22,14 +26,19 @@ export const addConnectionCount = async ( id, })); - const allResults = await client.find({ + const pitFinder = client.createPointInTimeFinder({ type: targetTypes, - page: 1, - perPage: 10000, + perPage: 1000, hasReference: references, hasReferenceOperator: 'OR', }); - allResults.saved_objects.forEach((obj) => { + + const results: SavedObject[] = []; + for await (const response of pitFinder.find()) { + results.push(...response.saved_objects); + } + + results.forEach((obj) => { obj.references.forEach((ref) => { if (ref.type === tagSavedObjectTypeName && ids.has(ref.id)) { counts.set(ref.id, counts.get(ref.id)! + 1); diff --git a/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.test.ts b/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.test.ts index 7d0eff3f77296..eb6f7eec3075f 100644 --- a/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.test.ts +++ b/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.test.ts @@ -194,11 +194,19 @@ describe('TagsClient', () => { it('calls `soClient.find` with the correct parameters', async () => { await tagsClient.getAll(); - expect(soClient.find).toHaveBeenCalledTimes(1); - expect(soClient.find).toHaveBeenCalledWith({ + expect(soClient.createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(soClient.createPointInTimeFinder).toHaveBeenCalledWith({ type: 'tag', - perPage: 10000, + perPage: 1000, }); + + expect(soClient.find).toHaveBeenCalledTimes(1); + expect(soClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'tag', + perPage: 1000, + }) + ); }); it('converts the objects returned from the soClient to tags', async () => { diff --git a/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.ts b/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.ts index 2b301652fa38d..32bb5d6e4e3eb 100644 --- a/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.ts +++ b/x-pack/plugins/saved_objects_tagging/server/services/tags/tags_client.ts @@ -48,12 +48,18 @@ export class TagsClient implements ITagsClient { } public async getAll() { - const result = await this.soClient.find({ + const pitFinder = this.soClient.createPointInTimeFinder({ type: this.type, - perPage: 10000, + perPage: 1000, }); - return result.saved_objects.map(savedObjectToTag); + const results: TagSavedObject[] = []; + for await (const response of pitFinder.find()) { + results.push(...response.saved_objects); + } + await pitFinder.close(); + + return results.map(savedObjectToTag); } public async delete(id: string) { diff --git a/x-pack/plugins/screenshotting/server/plugin.test.ts b/x-pack/plugins/screenshotting/server/plugin.test.ts new file mode 100644 index 0000000000000..22cd2c2f75ac5 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/plugin.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('./browsers/install'); + +import type { CoreSetup, CoreStart, PluginInitializerContext } from 'kibana/server'; +import { coreMock } from 'src/core/server/mocks'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; +import { install } from './browsers/install'; +import { ScreenshottingPlugin } from './plugin'; + +describe('ScreenshottingPlugin', () => { + let initContext: PluginInitializerContext; + let coreSetup: CoreSetup; + let coreStart: CoreStart; + let setupDeps: Parameters[1]; + let plugin: ScreenshottingPlugin; + + beforeEach(() => { + const configSchema = { + browser: { chromium: { disableSandbox: false } }, + }; + initContext = coreMock.createPluginInitializerContext(configSchema); + coreSetup = coreMock.createSetup({}); + coreStart = coreMock.createStart(); + setupDeps = { + screenshotMode: {} as ScreenshotModePluginSetup, + }; + plugin = new ScreenshottingPlugin(initContext); + }); + + describe('setup', () => { + test('returns a setup contract', async () => { + const setupContract = plugin.setup(coreSetup, setupDeps); + expect(setupContract).toEqual({}); + }); + + test('handles setup issues', async () => { + (install as jest.Mock).mockRejectedValue(`Unsupported platform!!!`); + + const setupContract = plugin.setup(coreSetup, setupDeps); + expect(setupContract).toEqual({}); + + await coreSetup.getStartServices(); + + const startContract = plugin.start(coreStart); + expect(startContract).toEqual( + expect.objectContaining({ + diagnose: expect.any(Function), + getScreenshots: expect.any(Function), + }) + ); + }); + }); + + describe('start', () => { + beforeEach(async () => { + plugin.setup(coreSetup, setupDeps); + await coreSetup.getStartServices(); + }); + + test('returns a start contract', async () => { + const startContract = plugin.start(coreStart); + expect(startContract).toEqual( + expect.objectContaining({ + diagnose: expect.any(Function), + getScreenshots: expect.any(Function), + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts index 53f855e1f544d..a301dd6764367 100755 --- a/x-pack/plugins/screenshotting/server/plugin.ts +++ b/x-pack/plugins/screenshotting/server/plugin.ts @@ -55,22 +55,21 @@ export class ScreenshottingPlugin implements Plugin { - try { - const paths = new ChromiumArchivePaths(); - const logger = this.logger.get('chromium'); - const [config, binaryPath] = await Promise.all([ - createConfig(this.logger, this.config), - install(paths, logger), - ]); + const paths = new ChromiumArchivePaths(); + const logger = this.logger.get('chromium'); + const [config, binaryPath] = await Promise.all([ + createConfig(this.logger, this.config), + install(paths, logger), + ]); - return new HeadlessChromiumDriverFactory(this.screenshotMode, config, logger, binaryPath); - } catch (error) { - this.logger.error('Error in screenshotting setup, it may not function properly.'); - - throw error; - } + return new HeadlessChromiumDriverFactory(this.screenshotMode, config, logger, binaryPath); })(); + this.browserDriverFactory.catch((error) => { + this.logger.error('Error in screenshotting setup, it may not function properly.'); + this.logger.error(error); + }); + return {}; } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 14e3c8cc95fe6..7fa387207e3ff 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -39,7 +39,7 @@ export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const; export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults' as const; export const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts' as const; export const DEFAULT_SIGNALS_INDEX = '.siem-signals' as const; -export const DEFAULT_PREVIEW_INDEX = '.siem-preview-signals' as const; +export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const; export const DEFAULT_LISTS_INDEX = '.lists' as const; export const DEFAULT_ITEMS_INDEX = '.items' as const; // The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts` @@ -256,8 +256,6 @@ export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = export const DETECTION_ENGINE_RULES_BULK_ACTION = `${DETECTION_ENGINE_RULES_URL}/_bulk_action` as const; export const DETECTION_ENGINE_RULES_PREVIEW = `${DETECTION_ENGINE_RULES_URL}/preview` as const; -export const DETECTION_ENGINE_RULES_PREVIEW_INDEX_URL = - `${DETECTION_ENGINE_RULES_PREVIEW}/index` as const; /** * Internal detection engine routes diff --git a/x-pack/plugins/security_solution/common/detection_engine/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/constants.ts new file mode 100644 index 0000000000000..7f3c822800673 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum RULE_PREVIEW_INVOCATION_COUNT { + HOUR = 20, + DAY = 24, + WEEK = 168, + MONTH = 30, +} diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index c5f4e5631e5c8..97079253606f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -370,6 +370,7 @@ export const previewRulesSchema = t.intersection([ createTypeSpecific, t.type({ invocationCount: t.number }), ]); +export type PreviewRulesSchema = t.TypeOf; type UpdateSchema = SharedUpdateSchema & T; export type EqlUpdateSchema = UpdateSchema>; diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 1b32f12dafd5b..250ff061e81dd 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -39,7 +39,7 @@ export interface SortColumnTimeline { export interface TimelinePersistInput { columns: ColumnHeaderOptions[]; dataProviders?: DataProvider[]; - dataViewId: string; + dataViewId: string | null; // null if legacy pre-8.0 timeline dateRange?: { start: string; end: string; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index 0e87378f4ef96..0e4dbc9a95f9c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -75,7 +75,9 @@ describe('CTI Enrichment', () => { it('Displays persisted enrichments on the JSON view', () => { const expectedEnrichment = [ { - feed: {}, + feed: { + name: 'AbuseCH malware', + }, indicator: { first_seen: '2021-03-10T08:02:14.000Z', file: { @@ -113,6 +115,7 @@ describe('CTI Enrichment', () => { it('Displays threat indicator details on the threat intel tab', () => { const expectedThreatIndicatorData = [ + { field: 'feed.name', value: 'AbuseCH malware' }, { field: 'indicator.file.hash.md5', value: '9b6c3518a91d23ed77504b5416bfb5b3' }, { field: 'indicator.file.hash.sha256', @@ -173,6 +176,7 @@ describe('CTI Enrichment', () => { const indicatorMatchRuleEnrichment = { field: 'myhash.mysha256', value: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + feedName: 'AbuseCH malware', }; const investigationTimeEnrichment = { field: 'source.ip', @@ -188,7 +192,7 @@ describe('CTI Enrichment', () => { .should('exist') .should( 'have.text', - `${indicatorMatchRuleEnrichment.field} ${indicatorMatchRuleEnrichment.value}` + `${indicatorMatchRuleEnrichment.field} ${indicatorMatchRuleEnrichment.value} from ${indicatorMatchRuleEnrichment.feedName}` ); cy.get(`${INVESTIGATION_TIME_ENRICHMENT_SECTION} ${THREAT_DETAILS_ACCORDION}`) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 02d8837261f2f..81022a43ff683 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -174,7 +174,7 @@ describe('Detection rules, threshold', () => { cy.get(ALERT_GRID_CELL).contains(rule.name); }); - it('Preview results of keyword using "host.name"', () => { + it.skip('Preview results of keyword using "host.name"', () => { rule.index = [...rule.index, '.siem-signals*']; createCustomRuleActivated(getNewRule()); @@ -188,7 +188,7 @@ describe('Detection rules, threshold', () => { cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); }); - it('Preview results of "ip" using "source.ip"', () => { + it.skip('Preview results of "ip" using "source.ip"', () => { const previewRule: ThresholdRule = { ...rule, thresholdField: 'source.ip', diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index aadaa5dfa0d88..a3e5e8af3f598 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -104,9 +104,9 @@ export const DEFINE_INDEX_INPUT = export const EQL_TYPE = '[data-test-subj="eqlRuleType"]'; -export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; +export const PREVIEW_HISTOGRAM = '[data-test-subj="preview-histogram-panel"]'; -export const EQL_QUERY_PREVIEW_HISTOGRAM = '[data-test-subj="queryPreviewEqlHistogram"]'; +export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loading"]'; @@ -170,7 +170,7 @@ export const RISK_OVERRIDE = export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]'; -export const RULES_CREATION_PREVIEW = '[data-test-subj="ruleCreationQueryPreview"]'; +export const RULES_CREATION_PREVIEW = '[data-test-subj="rule-preview"]'; export const RULE_DESCRIPTION_INPUT = '[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 68449363b8643..538f95c3c0a80 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -33,7 +33,6 @@ import { DEFAULT_RISK_SCORE_INPUT, DEFINE_CONTINUE_BUTTON, EQL_QUERY_INPUT, - EQL_QUERY_PREVIEW_HISTOGRAM, EQL_QUERY_VALIDATION_SPINNER, EQL_TYPE, FALSE_POSITIVES_INPUT, @@ -92,6 +91,7 @@ import { EMAIL_CONNECTOR_USER_INPUT, EMAIL_CONNECTOR_PASSWORD_INPUT, EMAIL_CONNECTOR_SERVICE_SELECTOR, + PREVIEW_HISTOGRAM, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -324,12 +324,12 @@ export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { .find(QUERY_PREVIEW_BUTTON) .should('not.be.disabled') .click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM) + cy.get(PREVIEW_HISTOGRAM) .invoke('text') .then((text) => { if (text !== 'Hits') { cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); + cy.get(PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); } }); cy.get(TOAST_ERROR).should('not.exist'); diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx index c16e77e9182f2..cfcf5307de8d4 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx @@ -12,6 +12,7 @@ import { SecurityPageName } from '../../../../common/constants'; import { createSecuritySolutionStorageMock, mockGlobalState, + mockIndexPattern, SUB_PLUGINS_REDUCER, TestProviders, } from '../../../common/mock'; @@ -36,6 +37,10 @@ jest.mock('../../../common/lib/kibana', () => { }; }); +jest.mock('../../../common/containers/source', () => ({ + useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }], +})); + jest.mock('react-reverse-portal', () => ({ InPortal: ({ children }: { children: React.ReactNode }) => <>{children}, OutPortal: ({ children }: { children: React.ReactNode }) => <>{children}, diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx index c283bb10c7928..6d09f369be044 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -24,10 +24,10 @@ export const SecuritySolutionBottomBar = React.memo( ({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => { const [showTimeline] = useShowTimeline(); - const { indicesExist } = useSourcererDataView(SourcererScopeName.timeline); - useResolveRedirect(); + const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline); - return indicesExist && showTimeline ? ( + useResolveRedirect(); + return (indicesExist || dataViewId === null) && showTimeline ? ( <> diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 751e8fde530ba..e6c8e1f6f8c4f 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -98,7 +98,7 @@ const CaseContainerComponent: React.FC = () => { timelineActions.createTimeline({ id: TimelineId.casePage, columns: [], - dataViewId: '', + dataViewId: null, indexNames: [], expandedDetail: {}, show: false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index afff935619740..617c66fa3c3da 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -327,11 +327,16 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const alertIdToClose = shouldCloseAlert && alertData ? alertData._id : undefined; const bulkCloseIndex = shouldBulkCloseAlert && signalIndexName != null ? [signalIndexName] : undefined; - addOrUpdateExceptionItems(ruleId, enrichExceptionItems(), alertIdToClose, bulkCloseIndex); + addOrUpdateExceptionItems( + maybeRule?.rule_id ?? '', + enrichExceptionItems(), + alertIdToClose, + bulkCloseIndex + ); } }, [ addOrUpdateExceptionItems, - ruleId, + maybeRule, enrichExceptionItems, shouldCloseAlert, shouldBulkCloseAlert, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 1724f616e7fc8..f88bf855049ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -267,11 +267,16 @@ export const EditExceptionModal = memo(function EditExceptionModal({ if (addOrUpdateExceptionItems !== null) { const bulkCloseIndex = shouldBulkCloseAlert && signalIndexName !== null ? [signalIndexName] : undefined; - addOrUpdateExceptionItems(ruleId, enrichExceptionItems(), undefined, bulkCloseIndex); + addOrUpdateExceptionItems( + maybeRule?.rule_id ?? '', + enrichExceptionItems(), + undefined, + bulkCloseIndex + ); } }, [ addOrUpdateExceptionItems, - ruleId, + maybeRule, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 0d7366557ff6e..564bb965a8782 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -44,16 +44,14 @@ describe('useAddOrUpdateException', () => { let updateExceptionListItem: jest.SpyInstance>; let getQueryFilter: jest.SpyInstance>; let buildAlertStatusesFilter: jest.SpyInstance< - ReturnType - >; - let buildAlertsRuleIdFilter: jest.SpyInstance< - ReturnType + ReturnType >; + let buildAlertsFilter: jest.SpyInstance>; let addOrUpdateItemsArgs: Parameters; let render: () => RenderHookResult; const onError = jest.fn(); const onSuccess = jest.fn(); - const ruleId = 'rule-id'; + const ruleStaticId = 'rule-id'; const alertIdToClose = 'idToClose'; const bulkCloseIndex = ['.custom']; const itemsToAdd: CreateExceptionListItemSchema[] = [ @@ -128,14 +126,11 @@ describe('useAddOrUpdateException', () => { getQueryFilter = jest.spyOn(getQueryFilterHelper, 'getQueryFilter'); - buildAlertStatusesFilter = jest.spyOn( - buildFilterHelpers, - 'buildAlertStatusesFilterRuleRegistry' - ); + buildAlertStatusesFilter = jest.spyOn(buildFilterHelpers, 'buildAlertStatusesFilter'); - buildAlertsRuleIdFilter = jest.spyOn(buildFilterHelpers, 'buildAlertsRuleIdFilter'); + buildAlertsFilter = jest.spyOn(buildFilterHelpers, 'buildAlertsFilter'); - addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate]; + addOrUpdateItemsArgs = [ruleStaticId, itemsToAddOrUpdate]; render = () => renderHook( () => @@ -262,7 +257,7 @@ describe('useAddOrUpdateException', () => { describe('when alertIdToClose is passed in', () => { beforeEach(() => { - addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate, alertIdToClose]; + addOrUpdateItemsArgs = [ruleStaticId, itemsToAddOrUpdate, alertIdToClose]; }); it('should update the alert status', async () => { await act(async () => { @@ -317,7 +312,7 @@ describe('useAddOrUpdateException', () => { describe('when bulkCloseIndex is passed in', () => { beforeEach(() => { - addOrUpdateItemsArgs = [ruleId, itemsToAddOrUpdate, undefined, bulkCloseIndex]; + addOrUpdateItemsArgs = [ruleStaticId, itemsToAddOrUpdate, undefined, bulkCloseIndex]; }); it('should update the status of only alerts that are open', async () => { await act(async () => { @@ -351,8 +346,8 @@ describe('useAddOrUpdateException', () => { addOrUpdateItems(...addOrUpdateItemsArgs); } await waitForNextUpdate(); - expect(buildAlertsRuleIdFilter).toHaveBeenCalledTimes(1); - expect(buildAlertsRuleIdFilter.mock.calls[0][0]).toEqual(ruleId); + expect(buildAlertsFilter).toHaveBeenCalledTimes(1); + expect(buildAlertsFilter.mock.calls[0][0]).toEqual(ruleStaticId); }); }); it('should generate the query filter using exceptions', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 7cb8b643aa0e8..71c49f7c2daad 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -17,27 +17,25 @@ import { HttpStart } from '../../../../../../../src/core/public'; import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api'; import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions'; import { - buildAlertsRuleIdFilter, + buildAlertsFilter, buildAlertStatusesFilter, - buildAlertStatusesFilterRuleRegistry, } from '../../../detections/components/alerts_table/default_config'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { Index } from '../../../../common/detection_engine/schemas/common/schemas'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers'; import { useKibana } from '../../lib/kibana'; /** * Adds exception items to the list. Also optionally closes alerts. * - * @param ruleId id of the rule where the exception updates will be applied + * @param ruleStaticId static id of the rule (rule.ruleId, not rule.id) where the exception updates will be applied * @param exceptionItemsToAddOrUpdate array of ExceptionListItemSchema to add or update * @param alertIdToClose - optional string representing alert to close * @param bulkCloseIndex - optional index used to create bulk close query * */ export type AddOrUpdateExceptionItemsFunc = ( - ruleId: string, + ruleStaticId: string, exceptionItemsToAddOrUpdate: Array, alertIdToClose?: string, bulkCloseIndex?: Index @@ -72,10 +70,10 @@ export const useAddOrUpdateException = ({ const addOrUpdateExceptionRef = useRef(null); const { addExceptionListItem, updateExceptionListItem } = useApi(services.http); const addOrUpdateException = useCallback( - async (ruleId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex) => { + async (ruleStaticId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex) => { if (addOrUpdateExceptionRef.current != null) { addOrUpdateExceptionRef.current( - ruleId, + ruleStaticId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex @@ -84,15 +82,13 @@ export const useAddOrUpdateException = ({ }, [] ); - // TODO: Once we are past experimental phase this code should be removed - const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); const onUpdateExceptionItemsAndAlertStatus: AddOrUpdateExceptionItemsFunc = async ( - ruleId, + ruleStaticId, exceptionItemsToAddOrUpdate, alertIdToClose, bulkCloseIndex @@ -131,15 +127,16 @@ export const useAddOrUpdateException = ({ } if (bulkCloseIndex != null) { - // TODO: Once we are past experimental phase this code should be removed - const alertStatusFilter = ruleRegistryEnabled - ? buildAlertStatusesFilterRuleRegistry(['open', 'acknowledged', 'in-progress']) - : buildAlertStatusesFilter(['open', 'acknowledged', 'in-progress']); + const alertStatusFilter = buildAlertStatusesFilter([ + 'open', + 'acknowledged', + 'in-progress', + ]); const filter = getQueryFilter( '', 'kuery', - [...buildAlertsRuleIdFilter(ruleId), ...alertStatusFilter], + [...buildAlertsFilter(ruleStaticId), ...alertStatusFilter], bulkCloseIndex, prepareExceptionItemsForBulkClose(exceptionItemsToAddOrUpdate), false @@ -185,14 +182,7 @@ export const useAddOrUpdateException = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [ - addExceptionListItem, - http, - onSuccess, - onError, - ruleRegistryEnabled, - updateExceptionListItem, - ]); + }, [addExceptionListItem, http, onSuccess, onError, updateExceptionListItem]); return [{ isLoading }, addOrUpdateException]; }; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index b67505a66be44..e5da55f740033 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -15,7 +15,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security import { UpdateDateRange } from '../charts/common'; import { GlobalTimeArgs } from '../../containers/use_global_time'; import { DocValueFields } from '../../../../common/search_strategy'; -import { Threshold } from '../../../detections/components/rules/query_preview'; +import { FieldValueThreshold } from '../../../detections/components/rules/threshold_input'; export type MatrixHistogramMappingTypes = Record< string, @@ -77,7 +77,7 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; histogramType: MatrixHistogramType; - threshold?: Threshold; + threshold?: FieldValueThreshold; skip?: boolean; isPtrIncluded?: boolean; includeMissingData?: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 9666a0837b046..d17f5ceb4f9b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -27,6 +27,10 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar z-index: 9900 !important; min-width: 24px; } + .euiPopover__panel.euiPopover__panel-isOpen.sourcererPopoverPanel { + // needs to appear under modal + z-index: 5900 !important; + } .euiToolTip { z-index: 9950 !important; } diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx index af21a018ee47a..3d378e72edbf4 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx @@ -14,7 +14,7 @@ import { EuiFormRow, EuiFormRowProps, } from '@elastic/eui'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { sourcererModel } from '../../store/sourcerer'; @@ -50,11 +50,25 @@ export const PopoverContent = styled.div` export const StyledBadge = styled(EuiBadge)` margin-left: 8px; + &, + .euiBadge__text { + cursor: pointer; + } +`; + +export const Blockquote = styled.span` + ${({ theme }) => css` + display: block; + border-color: ${theme.eui.euiColorDarkShade}; + border-left: ${theme.eui.euiBorderThick}; + margin: ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS} ${theme.eui.euiSizeS}; + padding: ${theme.eui.euiSizeS}; + `} `; interface GetDataViewSelectOptionsProps { dataViewId: string; - defaultDataView: sourcererModel.KibanaDataView; + defaultDataViewId: sourcererModel.KibanaDataView['id']; isModified: boolean; isOnlyDetectionAlerts: boolean; kibanaDataViews: sourcererModel.KibanaDataView[]; @@ -62,7 +76,7 @@ interface GetDataViewSelectOptionsProps { export const getDataViewSelectOptions = ({ dataViewId, - defaultDataView, + defaultDataViewId, isModified, isOnlyDetectionAlerts, kibanaDataViews, @@ -78,12 +92,12 @@ export const getDataViewSelectOptions = ({ ), - value: defaultDataView.id, + value: defaultDataViewId, }, ] : kibanaDataViews.map(({ title, id }) => ({ inputDisplay: - id === defaultDataView.id ? ( + id === defaultDataViewId ? ( {i18n.SECURITY_DEFAULT_DATA_VIEW_LABEL} {isModified && id === dataViewId && ( diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 763898378e6f4..7d3dc9641929a 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -19,8 +19,16 @@ import { } from '../../mock'; import { createStore } from '../../store'; import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control'; +import { waitFor } from '@testing-library/dom'; +import { useSourcererDataView } from '../../containers/sourcerer'; const mockDispatch = jest.fn(); + +jest.mock('../../containers/sourcerer'); +const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true); +jest.mock('./use_update_data_view', () => ({ + useUpdateDataView: () => mockUseUpdateDataView, +})); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); @@ -30,6 +38,15 @@ jest.mock('react-redux', () => { }; }); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); + const mockOptions = [ { label: 'apm-*-transaction*', value: 'apm-*-transaction*' }, { label: 'auditbeat-*', value: 'auditbeat-*' }, @@ -57,12 +74,21 @@ const patternListNoSignals = patternList .filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) .sort(); let store: ReturnType; +const sourcererDataView = { + indicesExist: true, + loading: false, +}; + describe('Sourcerer component', () => { const { storage } = createSecuritySolutionStorageMock(); beforeEach(() => { store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); jest.clearAllMocks(); + }); + + afterAll(() => { jest.restoreAllMocks(); }); @@ -215,7 +241,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, selectedDataViewId: '1234', selectedPatterns: ['filebeat-*'], }, @@ -267,7 +292,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, selectedDataViewId: id, selectedPatterns: patternListNoSignals.slice(0, 2), }, @@ -313,8 +337,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - patternList, selectedDataViewId: id, selectedPatterns: patternList.slice(0, 2), }, @@ -355,7 +377,6 @@ describe('Sourcerer component', () => { ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, selectedDataViewId: id, selectedPatterns: patternListNoSignals.slice(0, 2), }, @@ -629,6 +650,7 @@ describe('timeline sourcerer', () => { }; beforeAll(() => { + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); wrapper = mount( @@ -713,6 +735,7 @@ describe('timeline sourcerer', () => { }; store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + wrapper = mount( @@ -754,6 +777,7 @@ describe('Sourcerer integration tests', () => { const { storage } = createSecuritySolutionStorageMock(); beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView); store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); jest.clearAllMocks(); jest.restoreAllMocks(); @@ -795,11 +819,15 @@ describe('No data', () => { const { storage } = createSecuritySolutionStorageMock(); beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + indicesExist: false, + }); store = createStore(mockNoIndicesState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); jest.clearAllMocks(); jest.restoreAllMocks(); }); - test('Hide sourcerer', () => { + test('Hide sourcerer - default ', () => { const wrapper = mount( @@ -808,4 +836,123 @@ describe('No data', () => { expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); }); + test('Hide sourcerer - detections ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false); + }); + test('Hide sourcerer - timeline ', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).exists()).toEqual(true); + }); +}); + +describe('Update available', () => { + const { storage } = createSecuritySolutionStorageMock(); + const state2 = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'auditbeat-*', + patternList: ['auditbeat-*'], + }, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '12347', + title: 'packetbeat-*', + patternList: ['packetbeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + patternList, + selectedDataViewId: null, + selectedPatterns: ['myFakebeat-*'], + missingPatterns: ['myFakebeat-*'], + }, + }, + }, + }; + + let wrapper: ReactWrapper; + + beforeEach(() => { + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + activePatterns: ['myFakebeat-*'], + }); + store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + wrapper = mount( + + + + ); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Show Update available label', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-badge"]`).exists()).toBeTruthy(); + }); + + test('Show correct tooltip', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-tooltip"]`).prop('content')).toEqual( + 'myFakebeat-*' + ); + }); + + test('Show UpdateDefaultDataViewModal', () => { + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + + expect(wrapper.find(`UpdateDefaultDataViewModal`).prop('isShowing')).toEqual(true); + }); + + test('Show Add index pattern in UpdateDefaultDataViewModal', () => { + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + + expect(wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).text()).toEqual( + 'Add index pattern' + ); + }); + + test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => { + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click'); + + wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).simulate('click'); + + await waitFor(() => wrapper.update()); + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.timeline, + selectedDataViewId: 'security-solution', + selectedPatterns: ['myFakebeat-*'], + shouldValidateSelectedPatterns: false, + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 89bbeef72a21c..2ffb0670c4edc 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -13,13 +13,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiForm, + EuiOutsideClickDetector, EuiPopover, EuiPopoverTitle, EuiSpacer, EuiSuperSelect, - EuiToolTip, } from '@elastic/eui'; -import deepEqual from 'fast-deep-equal'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -27,18 +26,13 @@ import * as i18n from './translations'; import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer'; import { useDeepEqualSelector } from '../../hooks/use_selector'; import { SourcererScopeName } from '../../store/sourcerer/model'; -import { checkIfIndicesExist } from '../../store/sourcerer/helpers'; import { usePickIndexPatterns } from './use_pick_index_patterns'; -import { - FormRow, - getDataViewSelectOptions, - getTooltipContent, - PopoverContent, - ResetButton, - StyledBadge, - StyledButton, - StyledFormRow, -} from './helpers'; +import { FormRow, PopoverContent, ResetButton, StyledButton, StyledFormRow } from './helpers'; +import { TemporarySourcerer } from './temporary'; +import { UpdateDefaultDataViewModal } from './update_default_data_view_modal'; +import { useSourcererDataView } from '../../containers/sourcerer'; +import { useUpdateDataView } from './use_update_data_view'; +import { Trigger } from './trigger'; interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; @@ -54,13 +48,24 @@ export const Sourcerer = React.memo(({ scope: scopeId } defaultDataView, kibanaDataViews, signalIndexName, - sourcererScope: { selectedDataViewId, selectedPatterns, loading }, - sourcererDataView, + sourcererScope: { + selectedDataViewId, + selectedPatterns, + missingPatterns: sourcererMissingPatterns, + }, } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); - const indicesExist = useMemo( - () => checkIfIndicesExist({ scopeId, signalIndexName, sourcererDataView }), - [scopeId, signalIndexName, sourcererDataView] + + const { activePatterns, indicesExist, loading } = useSourcererDataView(scopeId); + const [missingPatterns, setMissingPatterns] = useState( + activePatterns && activePatterns.length > 0 + ? sourcererMissingPatterns.filter((p) => activePatterns.includes(p)) + : [] ); + useEffect(() => { + if (activePatterns && activePatterns.length > 0) { + setMissingPatterns(sourcererMissingPatterns.filter((p) => activePatterns.includes(p))); + } + }, [activePatterns, sourcererMissingPatterns]); const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState( isTimelineSourcerer && selectedPatterns.join() === signalIndexName @@ -68,15 +73,15 @@ export const Sourcerer = React.memo(({ scope: scopeId } const isOnlyDetectionAlerts: boolean = isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked); - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - const [dataViewId, setDataViewId] = useState(selectedDataViewId ?? defaultDataView.id); + const [dataViewId, setDataViewId] = useState(selectedDataViewId); const { + allOptions, + dataViewSelectOptions, isModified, onChangeCombo, renderOption, - selectableOptions, selectedOptions, setIndexPatternsByDataView, } = usePickIndexPatterns({ @@ -84,10 +89,12 @@ export const Sourcerer = React.memo(({ scope: scopeId } defaultDataViewId: defaultDataView.id, isOnlyDetectionAlerts, kibanaDataViews, + missingPatterns, scopeId, selectedPatterns, signalIndexName, }); + const onCheckboxChanged = useCallback( (e) => { setIsOnlyDetectionAlertsChecked(e.target.checked); @@ -96,20 +103,26 @@ export const Sourcerer = React.memo(({ scope: scopeId } }, [defaultDataView.id, setIndexPatternsByDataView] ); - const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]); + const [expandAdvancedOptions, setExpandAdvancedOptions] = useState(false); + const [isShowingUpdateModal, setIsShowingUpdateModal] = useState(false); const setPopoverIsOpenCb = useCallback(() => { setPopoverIsOpen((prevState) => !prevState); setExpandAdvancedOptions(false); // we always want setExpandAdvancedOptions collapsed by default when popover opened }, []); const onChangeDataView = useCallback( - (newSelectedDataView: string, newSelectedPatterns: string[]) => { + ( + newSelectedDataView: string, + newSelectedPatterns: string[], + shouldValidateSelectedPatterns?: boolean + ) => { dispatch( sourcererActions.setSelectedDataView({ id: scopeId, selectedDataViewId: newSelectedDataView, selectedPatterns: newSelectedPatterns, + shouldValidateSelectedPatterns, }) ); }, @@ -128,11 +141,14 @@ export const Sourcerer = React.memo(({ scope: scopeId } setDataViewId(defaultDataView.id); setIndexPatternsByDataView(defaultDataView.id); setIsOnlyDetectionAlertsChecked(false); + setMissingPatterns([]); }, [defaultDataView.id, setIndexPatternsByDataView]); const handleSaveIndices = useCallback(() => { const patterns = selectedOptions.map((so) => so.label); - onChangeDataView(dataViewId, patterns); + if (dataViewId != null) { + onChangeDataView(dataViewId, patterns); + } setPopoverIsOpen(false); }, [onChangeDataView, dataViewId, selectedOptions]); @@ -140,183 +156,220 @@ export const Sourcerer = React.memo(({ scope: scopeId } setPopoverIsOpen(false); setExpandAdvancedOptions(false); }, []); - const trigger = useMemo( - () => ( - - {i18n.DATA_VIEW} - {isModified === 'modified' && {i18n.MODIFIED_BADGE_TITLE}} - {isModified === 'alerts' && ( - - {i18n.ALERTS_BADGE_TITLE} - - )} - - ), - [isTimelineSourcerer, loading, setPopoverIsOpenCb, isModified] - ); - const dataViewSelectOptions = useMemo( - () => - getDataViewSelectOptions({ - dataViewId, - defaultDataView, - isModified: isModified === 'modified', - isOnlyDetectionAlerts, - kibanaDataViews, - }), - [dataViewId, defaultDataView, isModified, isOnlyDetectionAlerts, kibanaDataViews] - ); + // deprecated timeline index pattern handlers + const onContinueUpdateDeprecated = useCallback(() => { + setIsShowingUpdateModal(false); + const patterns = selectedPatterns.filter((pattern) => + defaultDataView.patternList.includes(pattern) + ); + onChangeDataView(defaultDataView.id, patterns); + setPopoverIsOpen(false); + }, [defaultDataView.id, defaultDataView.patternList, onChangeDataView, selectedPatterns]); + + const onUpdateDeprecated = useCallback(() => { + // are all the patterns in the default? + if (missingPatterns.length === 0) { + onContinueUpdateDeprecated(); + } else { + // open modal + setIsShowingUpdateModal(true); + } + }, [missingPatterns, onContinueUpdateDeprecated]); + + const [isTriggerDisabled, setIsTriggerDisabled] = useState(false); + + const onOpenAndReset = useCallback(() => { + setPopoverIsOpen(true); + resetDataSources(); + }, [resetDataSources]); + + const updateDataView = useUpdateDataView(onOpenAndReset); + const onUpdateDataView = useCallback(async () => { + const isUiSettingsSuccess = await updateDataView(missingPatterns); + setIsShowingUpdateModal(false); + setPopoverIsOpen(false); + + if (isUiSettingsSuccess) { + onChangeDataView( + defaultDataView.id, + // to be at this stage, activePatterns is defined, the ?? selectedPatterns is to make TS happy + activePatterns ?? selectedPatterns, + false + ); + setIsTriggerDisabled(true); + } + }, [ + activePatterns, + defaultDataView.id, + missingPatterns, + onChangeDataView, + selectedPatterns, + updateDataView, + ]); useEffect(() => { - setDataViewId((prevSelectedOption) => - selectedDataViewId != null && !deepEqual(selectedDataViewId, prevSelectedOption) - ? selectedDataViewId - : prevSelectedOption - ); + setDataViewId(selectedDataViewId); }, [selectedDataViewId]); - const tooltipContent = useMemo( - () => - getTooltipContent({ - isOnlyDetectionAlerts, - isPopoverOpen, - selectedPatterns, - signalIndexName, - }), - [isPopoverOpen, isOnlyDetectionAlerts, signalIndexName, selectedPatterns] - ); - - const buttonWithTooptip = useMemo(() => { - return tooltipContent ? ( - - {trigger} - - ) : ( - trigger - ); - }, [trigger, tooltipContent]); + const onOutsideClick = useCallback(() => { + setDataViewId(selectedDataViewId); + setMissingPatterns(sourcererMissingPatterns); + }, [selectedDataViewId, sourcererMissingPatterns]); const onExpandAdvancedOptionsClicked = useCallback(() => { setExpandAdvancedOptions((prevState) => !prevState); }, []); - return indicesExist ? ( + // always show sourcerer in timeline + return indicesExist || scopeId === SourcererScopeName.timeline ? ( + } closePopover={handleClosePopOver} + data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'} display="block" - repositionOnScroll + isOpen={isPopoverOpen} ownFocus + repositionOnScroll > - - - <>{i18n.SELECT_DATA_VIEW} - - {isOnlyDetectionAlerts && ( - - )} - - - {isTimelineSourcerer && ( - - - - )} - - - + + + <>{i18n.SELECT_DATA_VIEW} + + {isOnlyDetectionAlerts && ( + - - - - - - {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} - - {expandAdvancedOptions && } - - - + )} + + {isModified === 'deprecated' || isModified === 'missingPatterns' ? ( + <> + + setIsShowingUpdateModal(false)} + onContinue={onContinueUpdateDeprecated} + onUpdate={onUpdateDataView} + /> + + ) : ( + + <> + {isTimelineSourcerer && ( + + + + )} + {dataViewId && ( + + + + )} - {!isDetectionsSourcerer && ( - - - - - {i18n.INDEX_PATTERNS_RESET} - - - - + + {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} + + {expandAdvancedOptions && } + + - {i18n.SAVE_INDEX_PATTERNS} - - - - + isDisabled={isOnlyDetectionAlerts} + onChange={onChangeCombo} + options={allOptions} + placeholder={i18n.PICK_INDEX_PATTERNS} + renderOption={renderOption} + selectedOptions={selectedOptions} + /> + + + {!isDetectionsSourcerer && ( + + + + + {i18n.INDEX_PATTERNS_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + )} + + + )} - - - + + ) : null; }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/refresh_button.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/refresh_button.tsx new file mode 100644 index 0000000000000..c30c6aa2dea9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/refresh_button.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButton } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { RELOAD_PAGE_TITLE } from './translations'; + +const StyledRefreshButton = styled(EuiButton)` + float: right; +`; + +export const RefreshButton = React.memo(() => { + const onPageRefresh = useCallback(() => { + document.location.reload(); + }, []); + return ( + + {RELOAD_PAGE_TITLE} + + ); +}); + +RefreshButton.displayName = 'RefreshButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx new file mode 100644 index 0000000000000..36fae76c7739b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/temporary.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiCallOut, + EuiLink, + EuiText, + EuiTextColor, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import * as i18n from './translations'; +import { Blockquote, ResetButton } from './helpers'; + +interface Props { + activePatterns?: string[]; + indicesExist: boolean; + isModified: 'deprecated' | 'missingPatterns'; + missingPatterns: string[]; + onClick: () => void; + onClose: () => void; + onUpdate: () => void; + selectedPatterns: string[]; +} + +const translations = { + deprecated: { + title: i18n.CALL_OUT_DEPRECATED_TITLE, + update: i18n.UPDATE_INDEX_PATTERNS, + }, + missingPatterns: { + title: i18n.CALL_OUT_MISSING_PATTERNS_TITLE, + update: i18n.ADD_INDEX_PATTERN, + }, +}; + +export const TemporarySourcerer = React.memo( + ({ + activePatterns, + indicesExist, + isModified, + onClose, + onClick, + onUpdate, + selectedPatterns, + missingPatterns, + }) => { + const trigger = useMemo( + () => ( + + {translations[isModified].update} + + ), + [indicesExist, isModified, onUpdate] + ); + const buttonWithTooltip = useMemo( + () => + !indicesExist ? ( + + {trigger} + + ) : ( + trigger + ), + [indicesExist, trigger] + ); + + const deadPatterns = + activePatterns && activePatterns.length > 0 + ? selectedPatterns.filter((p) => !activePatterns.includes(p)) + : []; + + return ( + <> + + + + +

+ {activePatterns && activePatterns.length > 0 ? ( + 0 ? ( + !activePatterns.includes(p)) + .join(', '), + }} + /> + } + > + + + ) : null, + callout:

{activePatterns.join(', ')}
, + }} + /> + ) : ( + {selectedPatterns.join(', ')}, + }} + /> + )} + + {isModified === 'deprecated' && ( + {i18n.TOGGLE_TO_NEW_SOURCERER}, + }} + /> + )} + {isModified === 'missingPatterns' && ( + <> + {missingPatterns.join(', ')}, + }} + /> + {i18n.TOGGLE_TO_NEW_SOURCERER}, + }} + /> + + )} +

+
+
+ + + + {i18n.INDEX_PATTERNS_CLOSE} + + + {buttonWithTooltip} + + + ); + } +); + +TemporarySourcerer.displayName = 'TemporarySourcerer'; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts index fcf465ebfc9ef..2d8e506f39437 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts @@ -11,6 +11,20 @@ export const CALL_OUT_TITLE = i18n.translate('xpack.securitySolution.indexPatter defaultMessage: 'Data view cannot be modified on this page', }); +export const CALL_OUT_DEPRECATED_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.callOutDeprecxatedTitle', + { + defaultMessage: 'This timeline uses a legacy data view selector', + } +); + +export const CALL_OUT_MISSING_PATTERNS_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.callOutMissingPatternsTitle', + { + defaultMessage: 'This timeline is out of date with the Security Data View', + } +); + export const CALL_OUT_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.indexPatterns.callOutTimelineTitle', { @@ -18,9 +32,42 @@ export const CALL_OUT_TIMELINE_TITLE = i18n.translate( } ); +export const TOGGLE_TO_NEW_SOURCERER = i18n.translate( + 'xpack.securitySolution.indexPatterns.toggleToNewSourcerer.link', + { + defaultMessage: 'here', + } +); + export const DATA_VIEW = i18n.translate('xpack.securitySolution.indexPatterns.dataViewLabel', { defaultMessage: 'Data view', }); + +export const UPDATE_DATA_VIEW = i18n.translate( + 'xpack.securitySolution.indexPatterns.updateDataView', + { + defaultMessage: + 'Would you like to add this index pattern to Security Data View? Otherwise, we can recreate the data view without the missing index patterns.', + } +); + +export const UPDATE_SECURITY_DATA_VIEW = i18n.translate( + 'xpack.securitySolution.indexPatterns.updateSecurityDataView', + { + defaultMessage: 'Update Security Data View', + } +); + +export const CONTINUE_WITHOUT_ADDING = i18n.translate( + 'xpack.securitySolution.indexPatterns.continue', + { + defaultMessage: 'Continue without adding', + } +); +export const ADD_INDEX_PATTERN = i18n.translate('xpack.securitySolution.indexPatterns.add', { + defaultMessage: 'Add index pattern', +}); + export const MODIFIED_BADGE_TITLE = i18n.translate( 'xpack.securitySolution.indexPatterns.modifiedBadgeTitle', { @@ -35,6 +82,13 @@ export const ALERTS_BADGE_TITLE = i18n.translate( } ); +export const DEPRECATED_BADGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle', + { + defaultMessage: 'Update available', + } +); + export const SECURITY_DEFAULT_DATA_VIEW_LABEL = i18n.translate( 'xpack.securitySolution.indexPatterns.securityDefaultDataViewLabel', { @@ -97,6 +151,14 @@ export const DISABLED_INDEX_PATTERNS = i18n.translate( } ); +export const DISABLED_SOURCERER = i18n.translate('xpack.securitySolution.sourcerer.disabled', { + defaultMessage: 'The updates to the Data view require a page reload to take effect.', +}); + +export const UPDATE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.update', { + defaultMessage: 'Update and recreate data view', +}); + export const INDEX_PATTERNS_RESET = i18n.translate( 'xpack.securitySolution.indexPatterns.resetButton', { @@ -104,6 +166,22 @@ export const INDEX_PATTERNS_RESET = i18n.translate( } ); +export const INDEX_PATTERNS_CLOSE = i18n.translate( + 'xpack.securitySolution.indexPatterns.closeButton', + { + defaultMessage: 'Close', + } +); + +export const INACTIVE_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.inactive', { + defaultMessage: 'Inactive index patterns', +}); + +export const NO_DATA = i18n.translate('xpack.securitySolution.indexPatterns.noData', { + defaultMessage: + "The index pattern on this timeline doesn't match any data streams, indices, or index aliases.", +}); + export const PICK_INDEX_PATTERNS = i18n.translate( 'xpack.securitySolution.indexPatterns.pickIndexPatternsCombo', { @@ -117,3 +195,24 @@ export const ALERTS_CHECKBOX_LABEL = i18n.translate( defaultMessage: 'Show only detection alerts', } ); + +export const SUCCESS_TOAST_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.successToastTitle', + { + defaultMessage: 'One or more settings require you to reload the page to take effect', + } +); + +export const RELOAD_PAGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.reloadPageTitle', + { + defaultMessage: 'Reload page', + } +); + +export const FAILURE_TOAST_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.failureToastTitle', + { + defaultMessage: 'Unable to update data view', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx new file mode 100644 index 0000000000000..a464036f3b138 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/trigger.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, memo, useMemo } from 'react'; +import { EuiToolTip } from '@elastic/eui'; +import * as i18n from './translations'; +import { getTooltipContent, StyledBadge, StyledButton } from './helpers'; +import { ModifiedTypes } from './use_pick_index_patterns'; + +interface Props { + activePatterns?: string[]; + disabled: boolean; + isModified: ModifiedTypes; + isOnlyDetectionAlerts: boolean; + isPopoverOpen: boolean; + isTimelineSourcerer: boolean; + loading: boolean; + onClick: () => void; + selectedPatterns: string[]; + signalIndexName: string | null; +} +export const TriggerComponent: FC = ({ + activePatterns, + disabled, + isModified, + isOnlyDetectionAlerts, + isPopoverOpen, + isTimelineSourcerer, + loading, + onClick, + selectedPatterns, + signalIndexName, +}) => { + const badge = useMemo(() => { + switch (isModified) { + case 'modified': + return {i18n.MODIFIED_BADGE_TITLE}; + case 'alerts': + return ( + + {i18n.ALERTS_BADGE_TITLE} + + ); + case 'deprecated': + return ( + + {i18n.DEPRECATED_BADGE_TITLE} + + ); + case 'missingPatterns': + return ( + + {i18n.DEPRECATED_BADGE_TITLE} + + ); + case '': + default: + return null; + } + }, [isModified]); + + const trigger = useMemo( + () => ( + + {i18n.DATA_VIEW} + {!disabled && badge} + + ), + [disabled, badge, isTimelineSourcerer, loading, onClick] + ); + + const tooltipContent = useMemo( + () => + disabled + ? i18n.DISABLED_SOURCERER + : getTooltipContent({ + isOnlyDetectionAlerts, + isPopoverOpen, + // if activePatterns, use because we are in the temporary sourcerer state + selectedPatterns: activePatterns ?? selectedPatterns, + signalIndexName, + }), + [ + activePatterns, + disabled, + isOnlyDetectionAlerts, + isPopoverOpen, + selectedPatterns, + signalIndexName, + ] + ); + + return tooltipContent ? ( + + {trigger} + + ) : ( + trigger + ); +}; + +export const Trigger = memo(TriggerComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/update_default_data_view_modal.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/update_default_data_view_modal.tsx new file mode 100644 index 0000000000000..78fc6f82fa748 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/update_default_data_view_modal.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + EuiTextColor, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { Blockquote, ResetButton } from './helpers'; + +interface Props { + isShowing: boolean; + missingPatterns: string[]; + onClose: () => void; + onContinue: () => void; + onUpdate: () => void; +} +const MyEuiModal = styled(EuiModal)` + .euiModal__flex { + width: 60vw; + } + .euiCodeBlock { + height: auto !important; + max-width: 718px; + } + z-index: 99999999; +`; + +export const UpdateDefaultDataViewModal = React.memo( + ({ isShowing, onClose, onContinue, onUpdate, missingPatterns }) => + isShowing ? ( + + + +

{i18n.UPDATE_SECURITY_DATA_VIEW}

+
+
+ + + +

+ {missingPatterns.join(', ')}, + }} + /> + {i18n.UPDATE_DATA_VIEW} +

+
+
+ + + + {i18n.CONTINUE_WITHOUT_ADDING} + + + + + {i18n.ADD_INDEX_PATTERN} + + + +
+
+ ) : null +); + +UpdateDefaultDataViewModal.displayName = 'UpdateDefaultDataViewModal'; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx index 2ed2319499398..d7b094ab27b14 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx @@ -6,29 +6,31 @@ */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui'; import { getScopePatternListSelection } from '../../store/sourcerer/helpers'; import { sourcererModel } from '../../store/sourcerer'; -import { getPatternListWithoutSignals } from './helpers'; +import { getDataViewSelectOptions, getPatternListWithoutSignals } from './helpers'; import { SourcererScopeName } from '../../store/sourcerer/model'; interface UsePickIndexPatternsProps { - dataViewId: string; + dataViewId: string | null; defaultDataViewId: string; isOnlyDetectionAlerts: boolean; kibanaDataViews: sourcererModel.SourcererModel['kibanaDataViews']; + missingPatterns: string[]; scopeId: sourcererModel.SourcererScopeName; selectedPatterns: string[]; signalIndexName: string | null; } -export type ModifiedTypes = 'modified' | 'alerts' | ''; +export type ModifiedTypes = 'modified' | 'alerts' | 'deprecated' | 'missingPatterns' | ''; interface UsePickIndexPatterns { + allOptions: Array>; + dataViewSelectOptions: Array>; isModified: ModifiedTypes; onChangeCombo: (newSelectedDataViewId: Array>) => void; renderOption: ({ value }: EuiComboBoxOptionOption) => React.ReactElement; - selectableOptions: Array>; selectedOptions: Array>; setIndexPatternsByDataView: (newSelectedDataViewId: string, isAlerts?: boolean) => void; } @@ -45,6 +47,7 @@ export const usePickIndexPatterns = ({ defaultDataViewId, isOnlyDetectionAlerts, kibanaDataViews, + missingPatterns, scopeId, selectedPatterns, signalIndexName, @@ -54,42 +57,44 @@ export const usePickIndexPatterns = ({ [signalIndexName] ); - const { patternList, selectablePatterns } = useMemo(() => { + const { allPatterns, selectablePatterns } = useMemo<{ + allPatterns: string[]; + selectablePatterns: string[]; + }>(() => { if (isOnlyDetectionAlerts && signalIndexName) { return { - patternList: [signalIndexName], + allPatterns: [signalIndexName], selectablePatterns: [signalIndexName], }; } const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId); - return theDataView != null - ? scopeId === sourcererModel.SourcererScopeName.default - ? { - patternList: getPatternListWithoutSignals( - theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - signalIndexName - ), - selectablePatterns: getPatternListWithoutSignals( - theDataView.patternList, - signalIndexName - ), - } - : { - patternList: theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - selectablePatterns: theDataView.patternList, - } - : { patternList: [], selectablePatterns: [] }; + + if (theDataView == null) { + return { + allPatterns: [], + selectablePatterns: [], + }; + } + + const titleAsList = [...new Set(theDataView.title.split(','))]; + + return scopeId === sourcererModel.SourcererScopeName.default + ? { + allPatterns: getPatternListWithoutSignals(titleAsList, signalIndexName), + selectablePatterns: getPatternListWithoutSignals( + theDataView.patternList, + signalIndexName + ), + } + : { + allPatterns: titleAsList, + selectablePatterns: theDataView.patternList, + }; }, [dataViewId, isOnlyDetectionAlerts, kibanaDataViews, scopeId, signalIndexName]); - const selectableOptions = useMemo( - () => patternListToOptions(patternList, selectablePatterns), - [patternList, selectablePatterns] + const allOptions = useMemo( + () => patternListToOptions(allPatterns, selectablePatterns), + [allPatterns, selectablePatterns] ); const [selectedOptions, setSelectedOptions] = useState>>( isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns) @@ -111,37 +116,50 @@ export const usePickIndexPatterns = ({ ); const defaultSelectedPatternsAsOptions = useMemo( - () => getDefaultSelectedOptionsByDataView(dataViewId), + () => (dataViewId != null ? getDefaultSelectedOptionsByDataView(dataViewId) : []), [dataViewId, getDefaultSelectedOptionsByDataView] ); - const [isModified, setIsModified] = useState<'modified' | 'alerts' | ''>(''); + const [isModified, setIsModified] = useState( + dataViewId == null ? 'deprecated' : missingPatterns.length > 0 ? 'missingPatterns' : '' + ); const onSetIsModified = useCallback( - (patterns?: string[]) => { + (patterns: string[], id: string | null) => { + if (id == null) { + return setIsModified('deprecated'); + } + if (missingPatterns.length > 0) { + return setIsModified('missingPatterns'); + } if (isOnlyDetectionAlerts) { return setIsModified('alerts'); } - const modifiedPatterns = patterns != null ? patterns : selectedPatterns; const isPatternsModified = - defaultSelectedPatternsAsOptions.length !== modifiedPatterns.length || + defaultSelectedPatternsAsOptions.length !== patterns.length || !defaultSelectedPatternsAsOptions.every((option) => - modifiedPatterns.find((pattern) => option.value === pattern) + patterns.find((pattern) => option.value === pattern) ); return setIsModified(isPatternsModified ? 'modified' : ''); }, - [defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, selectedPatterns] + [defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, missingPatterns.length] ); - // when scope updates, check modified to set/remove alerts label useEffect(() => { setSelectedOptions( scopeId === SourcererScopeName.detections ? alertsOptions : patternListToOptions(selectedPatterns) ); - onSetIsModified(selectedPatterns); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scopeId, selectedPatterns]); + }, [selectedPatterns, scopeId]); + // when scope updates, check modified to set/remove alerts label + useEffect(() => { + onSetIsModified( + selectedOptions.map(({ label }) => label), + dataViewId + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataViewId, missingPatterns, scopeId, selectedOptions]); const onChangeCombo = useCallback((newSelectedOptions) => { setSelectedOptions(newSelectedOptions); @@ -156,11 +174,26 @@ export const usePickIndexPatterns = ({ setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts)); }; + const dataViewSelectOptions = useMemo( + () => + dataViewId != null + ? getDataViewSelectOptions({ + dataViewId, + defaultDataViewId, + isModified: isModified === 'modified', + isOnlyDetectionAlerts, + kibanaDataViews, + }) + : [], + [dataViewId, defaultDataViewId, isModified, isOnlyDetectionAlerts, kibanaDataViews] + ); + return { + allOptions, + dataViewSelectOptions, isModified, onChangeCombo, renderOption, - selectableOptions, selectedOptions, setIndexPatternsByDataView, }; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.test.tsx new file mode 100644 index 0000000000000..4ec39a60a97b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useUpdateDataView } from './use_update_data_view'; +import { useKibana } from '../../lib/kibana'; +import * as i18n from './translations'; +const mockAddSuccess = jest.fn(); +const mockAddError = jest.fn(); +const mockSet = jest.fn(); +const mockPatterns = ['packetbeat-*', 'winlogbeat-*']; +jest.mock('../../hooks/use_app_toasts', () => { + const original = jest.requireActual('../../hooks/use_app_toasts'); + + return { + ...original, + useAppToasts: () => ({ + addSuccess: mockAddSuccess, + addError: mockAddError, + }), + }; +}); +jest.mock('../../lib/kibana'); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public'); + + return { + ...original, + toMountPoint: jest.fn(), + }; +}); +describe('use_update_data_view', () => { + const mockError = jest.fn(); + beforeEach(() => { + (useKibana as jest.Mock).mockImplementation(() => ({ + services: { + uiSettings: { + get: () => mockPatterns, + set: mockSet.mockResolvedValue(true), + }, + }, + })); + jest.clearAllMocks(); + }); + + test('Successful uiSettings updates with correct index pattern, and shows success toast', async () => { + const { result } = renderHook(() => useUpdateDataView(mockError)); + const updateDataView = result.current; + const isUiSettingsSuccess = await updateDataView(['missing-*']); + expect(mockSet.mock.calls[0][1]).toEqual([...mockPatterns, 'missing-*'].sort()); + expect(isUiSettingsSuccess).toEqual(true); + expect(mockAddSuccess).toHaveBeenCalled(); + }); + + test('Failed uiSettings update returns false and shows error toast', async () => { + (useKibana as jest.Mock).mockImplementation(() => ({ + services: { + uiSettings: { + get: () => mockPatterns, + set: mockSet.mockResolvedValue(false), + }, + }, + })); + const { result } = renderHook(() => useUpdateDataView(mockError)); + const updateDataView = result.current; + const isUiSettingsSuccess = await updateDataView(['missing-*']); + expect(mockSet.mock.calls[0][1]).toEqual([...mockPatterns, 'missing-*'].sort()); + expect(isUiSettingsSuccess).toEqual(false); + expect(mockAddError).toHaveBeenCalled(); + expect(mockAddError.mock.calls[0][0]).toEqual(new Error(i18n.FAILURE_TOAST_TITLE)); + }); + + test('Failed uiSettings throws error and shows error toast', async () => { + (useKibana as jest.Mock).mockImplementation(() => ({ + services: { + uiSettings: { + get: jest.fn().mockImplementation(() => { + throw new Error('Uh oh bad times over here'); + }), + set: mockSet.mockResolvedValue(true), + }, + }, + })); + const { result } = renderHook(() => useUpdateDataView(mockError)); + const updateDataView = result.current; + const isUiSettingsSuccess = await updateDataView(['missing-*']); + expect(isUiSettingsSuccess).toEqual(false); + expect(mockAddError).toHaveBeenCalled(); + expect(mockAddError.mock.calls[0][0]).toEqual(new Error('Uh oh bad times over here')); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.tsx new file mode 100644 index 0000000000000..68193942ea257 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_update_data_view.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../lib/kibana'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { ensurePatternFormat } from '../../store/sourcerer/helpers'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import * as i18n from './translations'; +import { RefreshButton } from './refresh_button'; +import { useAppToasts } from '../../hooks/use_app_toasts'; + +export const useUpdateDataView = ( + onOpenAndReset: () => void +): ((missingPatterns: string[]) => Promise) => { + const { uiSettings } = useKibana().services; + const { addSuccess, addError } = useAppToasts(); + return useCallback( + async (missingPatterns: string[]): Promise => { + const asyncSearch = async (): Promise<[boolean, Error | null]> => { + try { + const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY); + const uiSettingsIndexPattern = [...defaultPatterns, ...missingPatterns]; + const isSuccess = await uiSettings.set( + DEFAULT_INDEX_KEY, + ensurePatternFormat(uiSettingsIndexPattern) + ); + return [isSuccess, null]; + } catch (e) { + return [false, e]; + } + }; + const [isUiSettingsSuccess, possibleError] = await asyncSearch(); + if (isUiSettingsSuccess) { + addSuccess({ + color: 'success', + title: toMountPoint(i18n.SUCCESS_TOAST_TITLE), + text: toMountPoint(), + iconType: undefined, + toastLifeTimeMs: 600000, + }); + return true; + } + addError(possibleError !== null ? possibleError : new Error(i18n.FAILURE_TOAST_TITLE), { + title: i18n.FAILURE_TOAST_TITLE, + toastMessage: ( + <> + + {i18n.TOGGLE_TO_NEW_SOURCERER} + + ), + }} + /> + + ) as unknown as string, + }); + return false; + }, + [addError, addSuccess, onOpenAndReset, uiSettings] + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 7c87aa19484bc..0f7e93f1befca 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -64,7 +64,7 @@ export const useSetInitialStateFromUrl = () => { dispatch( sourcererActions.setSelectedDataView({ id: scope, - selectedDataViewId: sourcererState[scope]?.id ?? '', + selectedDataViewId: sourcererState[scope]?.id ?? null, selectedPatterns: sourcererState[scope]?.selectedPatterns ?? [], }) ) diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 3311207eb1420..c493cb528d09a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -5,12 +5,16 @@ * 2.0. */ -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { matchPath } from 'react-router-dom'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; -import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model'; +import { + SelectedDataView, + SourcererDataView, + SourcererScopeName, +} from '../../store/sourcerer/model'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { @@ -28,6 +32,7 @@ import { checkIfIndicesExist, getScopePatternListSelection } from '../../store/s import { useAppToasts } from '../../hooks/use_app_toasts'; import { postSourcererDataView } from './api'; import { useDataView } from '../source/use_data_view'; +import { useFetchIndex } from '../source'; export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default @@ -37,11 +42,14 @@ export const useInitSourcerer = ( const initialTimelineSourcerer = useRef(true); const initialDetectionSourcerer = useRef(true); const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); - const getDefaultDataViewSelector = useMemo( - () => sourcererSelectors.defaultDataViewSelector(), + + const getDataViewsSelector = useMemo( + () => sourcererSelectors.getSourcererDataViewsSelector(), [] ); - const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector); + const { defaultDataView, signalIndexName: signalIndexNameSourcerer } = useDeepEqualSelector( + (state) => getDataViewsSelector(state) + ); const { addError } = useAppToasts(); @@ -59,12 +67,6 @@ export const useInitSourcerer = ( } }, [addError, defaultDataView.error]); - const getSignalIndexNameSelector = useMemo( - () => sourcererSelectors.signalIndexNameSelector(), - [] - ); - const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector); - const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const activeTimeline = useDeepEqualSelector((state) => getTimelineSelector(state, TimelineId.active) @@ -256,14 +258,26 @@ export const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*'; export const useSourcererDataView = ( scopeId: SourcererScopeName = SourcererScopeName.default ): SelectedDataView => { - const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); + const { getDataViewsSelector, getSourcererDataViewSelector, getScopeSelector } = useMemo( + () => ({ + getDataViewsSelector: sourcererSelectors.getSourcererDataViewsSelector(), + getSourcererDataViewSelector: sourcererSelectors.sourcererDataViewSelector(), + getScopeSelector: sourcererSelectors.scopeIdSelector(), + }), + [] + ); const { signalIndexName, - sourcererDataView: selectedDataView, - sourcererScope: { selectedPatterns: scopeSelectedPatterns, loading }, - }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => - sourcererScopeSelector(state, scopeId) - ); + selectedDataView, + sourcererScope: { missingPatterns, selectedPatterns: scopeSelectedPatterns, loading }, + }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => { + const sourcererScope = getScopeSelector(state, scopeId); + return { + ...getDataViewsSelector(state), + selectedDataView: getSourcererDataViewSelector(state, sourcererScope.selectedDataViewId), + sourcererScope, + }; + }); const selectedPatterns = useMemo( () => @@ -273,40 +287,69 @@ export const useSourcererDataView = ( [scopeSelectedPatterns] ); + const [legacyPatterns, setLegacyPatterns] = useState([]); + + const [indexPatternsLoading, fetchIndexReturn] = useFetchIndex(legacyPatterns); + + const legacyDataView: Omit & { id: string | null } = useMemo( + () => ({ + ...fetchIndexReturn, + runtimeMappings: {}, + title: '', + id: selectedDataView?.id ?? null, + loading: indexPatternsLoading, + patternList: fetchIndexReturn.indexes, + indexFields: fetchIndexReturn.indexPatterns + .fields as SelectedDataView['indexPattern']['fields'], + }), + [fetchIndexReturn, indexPatternsLoading, selectedDataView] + ); + + useEffect(() => { + if (selectedDataView == null || missingPatterns.length > 0) { + // old way of fetching indices, legacy timeline + setLegacyPatterns(selectedPatterns); + } else { + setLegacyPatterns([]); + } + }, [missingPatterns, selectedDataView, selectedPatterns]); + + const sourcererDataView = useMemo( + () => + selectedDataView == null || missingPatterns.length > 0 ? legacyDataView : selectedDataView, + [legacyDataView, missingPatterns.length, selectedDataView] + ); + const indicesExist = useMemo( - () => checkIfIndicesExist({ scopeId, signalIndexName, sourcererDataView: selectedDataView }), - [scopeId, signalIndexName, selectedDataView] + () => + checkIfIndicesExist({ + scopeId, + signalIndexName, + patternList: sourcererDataView.patternList, + }), + [scopeId, signalIndexName, sourcererDataView] ); return useMemo( () => ({ - browserFields: selectedDataView.browserFields, - dataViewId: selectedDataView.id, - docValueFields: selectedDataView.docValueFields, + browserFields: sourcererDataView.browserFields, + dataViewId: sourcererDataView.id, + docValueFields: sourcererDataView.docValueFields, indexPattern: { - fields: selectedDataView.indexFields, + fields: sourcererDataView.indexFields, title: selectedPatterns.join(','), }, indicesExist, - loading: loading || selectedDataView.loading, - runtimeMappings: selectedDataView.runtimeMappings, + loading: loading || sourcererDataView.loading, + runtimeMappings: sourcererDataView.runtimeMappings, // all active & inactive patterns in DATA_VIEW - patternList: selectedDataView.title.split(','), - // selected patterns in DATA_VIEW + patternList: sourcererDataView.title.split(','), + // selected patterns in DATA_VIEW including filter selectedPatterns: selectedPatterns.sort(), + // if we have to do an update to data view, tell us which patterns are active + ...(legacyPatterns.length > 0 ? { activePatterns: sourcererDataView.patternList } : {}), }), - [ - selectedDataView.browserFields, - selectedDataView.id, - selectedDataView.docValueFields, - selectedDataView.indexFields, - selectedDataView.loading, - selectedDataView.runtimeMappings, - selectedDataView.title, - selectedPatterns, - indicesExist, - loading, - ] + [sourcererDataView, selectedPatterns, indicesExist, loading, legacyPatterns.length] ); }; diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 0f814d758e7f5..2de29a8c3acf8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -1955,7 +1955,7 @@ export const mockTimelineModel: TimelineModel = { columns: mockTimelineModelColumns, defaultColumns: mockTimelineModelColumns, dataProviders: [], - dataViewId: '', + dataViewId: null, dateRange: { end: '2020-03-18T13:52:38.929Z', start: '2020-03-18T13:46:38.929Z', @@ -2092,7 +2092,7 @@ export const defaultTimelineProps: CreateTimelineProps = { queryMatch: { field: '_id', operator: ':', value: '1' }, }, ], - dataViewId: '', + dataViewId: null, dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' }, deletedEventIds: [], description: '', diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts index 1cbf08c354b33..e46a4a532d701 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts @@ -8,7 +8,7 @@ import { parseExperimentalConfigValue } from '../../../common/experimental_features'; import { SecuritySubPlugins } from '../../app/types'; import { createInitialState } from './reducer'; -import { mockSourcererState } from '../mock'; +import { mockIndexPattern, mockSourcererState } from '../mock'; import { useSourcererDataView } from '../containers/sourcerer'; import { useDeepEqualSelector } from '../hooks/use_selector'; import { renderHook } from '@testing-library/react-hooks'; @@ -19,6 +19,12 @@ jest.mock('../lib/kibana', () => ({ get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), }, })); +jest.mock('../containers/source', () => ({ + useFetchIndex: () => [ + false, + { indexes: [], indicesExist: true, indexPatterns: mockIndexPattern }, + ], +})); describe('createInitialState', () => { describe('sourcerer -> default -> indicesExist', () => { @@ -40,20 +46,24 @@ describe('createInitialState', () => { (useDeepEqualSelector as jest.Mock).mockClear(); }); - test('indicesExist should be TRUE if configIndexPatterns is NOT empty', async () => { + test('indicesExist should be TRUE if patternList is NOT empty', async () => { const { result } = renderHook(() => useSourcererDataView()); expect(result.current.indicesExist).toEqual(true); }); - test('indicesExist should be FALSE if configIndexPatterns is empty', () => { + test('indicesExist should be FALSE if patternList is empty', () => { const state = createInitialState(mockPluginState, { ...defaultState, defaultDataView: { ...defaultState.defaultDataView, - id: '', - title: '', patternList: [], }, + kibanaDataViews: [ + { + ...defaultState.defaultDataView, + patternList: [], + }, + ], }); (useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state)); const { result } = renderHook(() => useSourcererDataView()); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts index aa0689de9cca3..6a3d3e71f3750 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts @@ -6,9 +6,8 @@ */ import actionCreatorFactory from 'typescript-fsa'; -import { TimelineEventsType } from '../../../../common/types/timeline'; -import { SourcererDataView, SourcererScopeName } from './model'; +import { SelectedDataView, SourcererDataView, SourcererScopeName } from './model'; import { SecurityDataView } from '../../containers/sourcerer/api'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer'); @@ -39,8 +38,8 @@ export const setSourcererScopeLoading = actionCreator<{ export interface SelectedDataViewPayload { id: SourcererScopeName; - selectedDataViewId: string; - selectedPatterns: string[]; - eventType?: TimelineEventsType; + selectedDataViewId: SelectedDataView['dataViewId']; + selectedPatterns: SelectedDataView['selectedPatterns']; + shouldValidateSelectedPatterns?: boolean; } export const setSelectedDataView = actionCreator('SET_SELECTED_DATA_VIEW'); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts index 5945b453673c3..672ecb575ce79 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.test.ts @@ -69,7 +69,7 @@ describe('sourcerer store helpers', () => { selectedPatterns: ['auditbeat-*'], }; it('sets selectedPattern', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload); + const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload, true); expect(result).toEqual({ [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], @@ -78,10 +78,14 @@ describe('sourcerer store helpers', () => { }); }); it('sets to default when empty array is passed and scope is default', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - selectedPatterns: [], - }); + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + selectedPatterns: [], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], @@ -90,11 +94,15 @@ describe('sourcerer store helpers', () => { }); }); it('sets to default when empty array is passed and scope is detections', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.detections, - selectedPatterns: [], - }); + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.detections, + selectedPatterns: [], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.detections]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.detections], @@ -103,22 +111,21 @@ describe('sourcerer store helpers', () => { }, }); }); - it('sets to default when empty array is passed and scope is timeline', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.timeline, - selectedPatterns: [], - }); + it('sets to empty when empty array is passed and scope is timeline', () => { + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.timeline, + selectedPatterns: [], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], selectedDataViewId: dataView.id, - selectedPatterns: [ - signalIndexName, - ...mockGlobalState.sourcerer.defaultDataView.patternList.filter( - (p) => p !== signalIndexName - ), - ].sort(), + selectedPatterns: [], }, }); }); @@ -132,11 +139,15 @@ describe('sourcerer store helpers', () => { defaultDataView: dataViewNoSignals, kibanaDataViews: [dataViewNoSignals], }; - const result = validateSelectedPatterns(stateNoSignals, { - ...payload, - id: SourcererScopeName.timeline, - selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`], - }); + const result = validateSelectedPatterns( + stateNoSignals, + { + ...payload, + id: SourcererScopeName.timeline, + selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], @@ -147,19 +158,23 @@ describe('sourcerer store helpers', () => { }); describe('handles missing dataViewId, 7.16 -> 8.0', () => { it('selectedPatterns.length > 0 & all selectedPatterns exist in defaultDataView, set dataViewId to defaultDataView.id', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.timeline, - selectedDataViewId: '', - selectedPatterns: [ - mockGlobalState.sourcerer.defaultDataView.patternList[3], - mockGlobalState.sourcerer.defaultDataView.patternList[4], - ], - }); + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.timeline, + selectedDataViewId: null, + selectedPatterns: [ + mockGlobalState.sourcerer.defaultDataView.patternList[3], + mockGlobalState.sourcerer.defaultDataView.patternList[4], + ], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - selectedDataViewId: dataView.id, + selectedDataViewId: null, selectedPatterns: [ mockGlobalState.sourcerer.defaultDataView.patternList[3], mockGlobalState.sourcerer.defaultDataView.patternList[4], @@ -167,16 +182,20 @@ describe('sourcerer store helpers', () => { }, }); }); - it('selectedPatterns.length > 0 & a pattern in selectedPatterns does not exist in defaultDataView, set dataViewId to null', () => { - const result = validateSelectedPatterns(mockGlobalState.sourcerer, { - ...payload, - id: SourcererScopeName.timeline, - selectedDataViewId: '', - selectedPatterns: [ - mockGlobalState.sourcerer.defaultDataView.patternList[3], - 'journalbeat-*', - ], - }); + it('selectedPatterns.length > 0 & some selectedPatterns do not exist in defaultDataView, set dataViewId to null', () => { + const result = validateSelectedPatterns( + mockGlobalState.sourcerer, + { + ...payload, + id: SourcererScopeName.timeline, + selectedDataViewId: null, + selectedPatterns: [ + mockGlobalState.sourcerer.defaultDataView.patternList[3], + 'journalbeat-*', + ], + }, + true + ); expect(result).toEqual({ [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], @@ -185,6 +204,7 @@ describe('sourcerer store helpers', () => { mockGlobalState.sourcerer.defaultDataView.patternList[3], 'journalbeat-*', ], + missingPatterns: ['journalbeat-*'], }, }); }); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts index 689bf1c4502d8..7f176b0efaca4 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts @@ -34,54 +34,58 @@ export const getScopePatternListSelection = ( } }; +export const ensurePatternFormat = (patternList: string[]): string[] => + [ + ...new Set( + patternList.reduce((acc: string[], pattern: string) => [...pattern.split(','), ...acc], []) + ), + ].sort(); + export const validateSelectedPatterns = ( state: SourcererModel, - payload: SelectedDataViewPayload + payload: SelectedDataViewPayload, + shouldValidateSelectedPatterns: boolean ): Partial => { const { id, ...rest } = payload; - let dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId); + const dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId); // dedupe because these could come from a silly url or pre 8.0 timeline - const dedupePatterns = [...new Set(rest.selectedPatterns)]; - let selectedPatterns = - dataView != null + const dedupePatterns = ensurePatternFormat(rest.selectedPatterns); + let missingPatterns: string[] = []; + // check for missing patterns against default data view only + if (dataView == null || dataView.id === state.defaultDataView.id) { + const dedupeAllDefaultPatterns = ensurePatternFormat( + (dataView ?? state.defaultDataView).title.split(',') + ); + missingPatterns = dedupePatterns.filter( + (pattern) => !dedupeAllDefaultPatterns.includes(pattern) + ); + } + const selectedPatterns = + // shouldValidateSelectedPatterns is false when upgrading from + // legacy pre-8.0 timeline index patterns to data view. + shouldValidateSelectedPatterns && dataView != null && missingPatterns.length === 0 ? dedupePatterns.filter( (pattern) => - // Typescript is being mean and telling me dataView could be undefined here - // so redoing the dataView != null check (dataView != null && dataView.patternList.includes(pattern)) || // this is a hack, but sometimes signal index is deleted and is getting regenerated. it gets set before it is put in the dataView state.signalIndexName == null || state.signalIndexName === pattern ) - : // 7.16 -> 8.0 this will get hit because dataView == null + : // don't remove non-existing patterns, they were saved in the first place in timeline + // but removed from the security data view + // or its a legacy pre-8.0 timeline dedupePatterns; - if (selectedPatterns.length > 0 && dataView == null) { - // we have index patterns, but not a data view id - // find out if we have these index patterns in the defaultDataView - const areAllPatternsInDefault = selectedPatterns.every( - (pattern) => state.defaultDataView.title.indexOf(pattern) > -1 - ); - if (areAllPatternsInDefault) { - dataView = state.defaultDataView; - selectedPatterns = selectedPatterns.filter( - (pattern) => dataView != null && dataView.patternList.includes(pattern) - ); - } - } - // TO DO: Steph/sourcerer If dataView is still undefined here, create temporary dataView - // and prompt user to go create this dataView - // currently UI will take the undefined dataView and default to defaultDataView anyways - // this is a "strategically merged" bug ;) - // https://github.com/elastic/security-team/issues/1921 - return { [id]: { ...state.sourcererScopes[id], ...rest, selectedDataViewId: dataView?.id ?? null, selectedPatterns, - ...(isEmpty(selectedPatterns) + missingPatterns, + // if in timeline, allow for empty in case pattern was deleted + // need flow for this + ...(isEmpty(selectedPatterns) && id !== SourcererScopeName.timeline ? { selectedPatterns: getScopePatternListSelection( dataView ?? state.defaultDataView, @@ -97,17 +101,17 @@ export const validateSelectedPatterns = ( }; interface CheckIfIndicesExistParams { + patternList: sourcererModel.SourcererDataView['patternList']; scopeId: sourcererModel.SourcererScopeName; signalIndexName: string | null; - sourcererDataView: sourcererModel.SourcererDataView; } export const checkIfIndicesExist = ({ + patternList, scopeId, signalIndexName, - sourcererDataView, }: CheckIfIndicesExistParams) => scopeId === SourcererScopeName.detections - ? sourcererDataView.patternList.includes(`${signalIndexName}`) + ? patternList.includes(`${signalIndexName}`) : scopeId === SourcererScopeName.default - ? sourcererDataView.patternList.filter((i) => i !== signalIndexName).length > 0 - : sourcererDataView.patternList.length > 0; + ? patternList.filter((i) => i !== signalIndexName).length > 0 + : patternList.length > 0; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts index a22a04d025d19..61377662fa812 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts @@ -29,10 +29,15 @@ export interface SourcererScope { id: SourcererScopeName; /** is an update being made to the sourcerer data view */ loading: boolean; - /** selected data view id */ - selectedDataViewId: string; + /** selected data view id, null if it is legacy index patterns*/ + selectedDataViewId: string | null; /** selected patterns within the data view */ selectedPatterns: string[]; + /** if has length, + * id === SourcererScopeName.timeline + * selectedDataViewId === null OR defaultDataView.id + * saved timeline has pattern that is not in the default */ + missingPatterns: string[]; } export type SourcererScopeById = Record; @@ -54,6 +59,7 @@ export interface KibanaDataView { * DataView from Kibana + timelines/index_fields enhanced field data */ export interface SourcererDataView extends KibanaDataView { + id: string; /** we need this for @timestamp data */ browserFields: BrowserFields; /** we need this for @timestamp data */ @@ -75,7 +81,7 @@ export interface SourcererDataView extends KibanaDataView { */ export interface SelectedDataView { browserFields: SourcererDataView['browserFields']; - dataViewId: SourcererDataView['id']; + dataViewId: string | null; // null if legacy pre-8.0 timeline docValueFields: SourcererDataView['docValueFields']; /** * DataViewBase with enhanced index fields used in timelines @@ -88,8 +94,10 @@ export interface SelectedDataView { /** all active & inactive patterns from SourcererDataView['title'] */ patternList: string[]; runtimeMappings: SourcererDataView['runtimeMappings']; - /** all selected patterns from SourcererScope['selectedPatterns'] */ - selectedPatterns: string[]; + /** all selected patterns from SourcererScope['selectedPatterns'] */ + selectedPatterns: SourcererScope['selectedPatterns']; + // active patterns when dataViewId == null + activePatterns?: string[]; } /** @@ -97,7 +105,7 @@ export interface SelectedDataView { */ export interface SourcererModel { /** default security-solution data view */ - defaultDataView: SourcererDataView & { error?: unknown }; + defaultDataView: SourcererDataView & { id: string; error?: unknown }; /** all Kibana data views, including security-solution */ kibanaDataViews: SourcererDataView[]; /** security solution signals index name */ @@ -115,8 +123,9 @@ export type SourcererUrlState = Partial<{ export const initSourcererScope: Omit = { loading: false, - selectedDataViewId: '', + selectedDataViewId: null, selectedPatterns: [], + missingPatterns: [], }; export const initDataView = { browserFields: EMPTY_BROWSER_FIELDS, diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts index e1747a6786cdb..648ba354f29d9 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts @@ -72,13 +72,17 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState) }), }, })) - .case(setSelectedDataView, (state, payload) => ({ - ...state, - sourcererScopes: { - ...state.sourcererScopes, - ...validateSelectedPatterns(state, payload), - }, - })) + .case(setSelectedDataView, (state, payload) => { + const { shouldValidateSelectedPatterns = true, ...patternsInfo } = payload; + + return { + ...state, + sourcererScopes: { + ...state.sourcererScopes, + ...validateSelectedPatterns(state, patternsInfo, shouldValidateSelectedPatterns), + }, + }; + }) .case(setDataView, (state, dataView) => ({ ...state, ...(dataView.id === state.defaultDataView.id diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index b72d7bfde2dcc..8c0b1ecf6f627 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -26,8 +26,11 @@ export const sourcererDefaultDataViewSelector = ({ sourcerer, }: State): SourcererModel['defaultDataView'] => sourcerer.defaultDataView; -export const dataViewSelector = ({ sourcerer }: State, id: string): SourcererDataView => - sourcerer.kibanaDataViews.find((dataView) => dataView.id === id) ?? sourcerer.defaultDataView; +export const dataViewSelector = ( + { sourcerer }: State, + id: string | null +): SourcererDataView | undefined => + sourcerer.kibanaDataViews.find((dataView) => dataView.id === id); export const sourcererScopeIdSelector = ( { sourcerer }: State, @@ -54,29 +57,48 @@ export const sourcererDataViewSelector = () => createSelector(dataViewSelector, (dataView) => dataView); export interface SourcererScopeSelector extends Omit { - sourcererDataView: SourcererDataView; + selectedDataView: SourcererDataView | undefined; sourcererScope: SourcererScope; } -export const getSourcererScopeSelector = () => { +export const getSourcererDataViewsSelector = () => { const getKibanaDataViewsSelector = kibanaDataViewsSelector(); const getDefaultDataViewSelector = defaultDataViewSelector(); const getSignalIndexNameSelector = signalIndexNameSelector(); - const getSourcererDataViewSelector = sourcererDataViewSelector(); - const getScopeSelector = scopeIdSelector(); - - return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { + return (state: State): Omit => { const kibanaDataViews = getKibanaDataViewsSelector(state); const defaultDataView = getDefaultDataViewSelector(state); const signalIndexName = getSignalIndexNameSelector(state); - const scope = getScopeSelector(state, scopeId); - const sourcererDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); return { defaultDataView, kibanaDataViews, signalIndexName, - sourcererDataView, + }; + }; +}; + +/** + * Attn Future Developer + * Access sourcererScope.selectedPatterns from + * hook useSourcererDataView in `common/containers/sourcerer/index` + * in order to get exclude patterns for searches + * Access sourcererScope.selectedPatterns + * from this function for display purposes only + * */ +export const getSourcererScopeSelector = () => { + const getDataViewsSelector = getSourcererDataViewsSelector(); + const getSourcererDataViewSelector = sourcererDataViewSelector(); + const getScopeSelector = scopeIdSelector(); + + return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { + const dataViews = getDataViewsSelector(state); + const scope = getScopeSelector(state, scopeId); + const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId); + + return { + ...dataViews, + selectedDataView, sourcererScope: scope, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 73af793275122..a7d443acc3daf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -140,7 +140,7 @@ describe('alert actions', () => { ], defaultColumns: defaultHeaders, dataProviders: [], - dataViewId: '', + dataViewId: null, dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx index 13e93604863b4..aab6cabdb3a93 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.test.tsx @@ -7,7 +7,7 @@ import { ExistsFilter, Filter } from '@kbn/es-query'; import { - buildAlertsRuleIdFilter, + buildAlertsFilter, buildAlertStatusesFilter, buildAlertStatusFilter, buildThreatMatchFilter, @@ -18,21 +18,21 @@ jest.mock('./actions'); describe('alerts default_config', () => { describe('buildAlertsRuleIdFilter', () => { test('given a rule id this will return an array with a single filter', () => { - const filters: Filter[] = buildAlertsRuleIdFilter('rule-id-1'); + const filters: Filter[] = buildAlertsFilter('rule-id-1'); const expectedFilter: Filter = { meta: { alias: null, negate: false, disabled: false, type: 'phrase', - key: 'kibana.alert.rule.uuid', + key: 'kibana.alert.rule.rule_id', params: { query: 'rule-id-1', }, }, query: { match_phrase: { - 'kibana.alert.rule.uuid': 'rule-id-1', + 'kibana.alert.rule.rule_id': 'rule-id-1', }, }, }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index a5947e45ed0f0..663d133f04b1c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -6,21 +6,13 @@ */ import { - ALERT_DURATION, - ALERT_RULE_PRODUCER, - ALERT_START, + ALERT_BUILDING_BLOCK_TYPE, ALERT_WORKFLOW_STATUS, - ALERT_UUID, - ALERT_RULE_UUID, - ALERT_RULE_NAME, - ALERT_RULE_CATEGORY, - ALERT_RULE_SEVERITY, - ALERT_RULE_RISK_SCORE, + ALERT_RULE_RULE_ID, } from '@kbn/rule-data-utils/technical_field_names'; import type { Filter } from '@kbn/es-query'; -import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline'; +import { RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; @@ -34,12 +26,12 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => { should: [ { term: { - 'kibana.alert.workflow_status': status, + [ALERT_WORKFLOW_STATUS]: status, }, }, { term: { - 'kibana.alert.workflow_status': 'in-progress', + [ALERT_WORKFLOW_STATUS]: 'in-progress', }, }, ], @@ -47,7 +39,7 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => { } : { term: { - 'kibana.alert.workflow_status': status, + [ALERT_WORKFLOW_STATUS]: status, }, }; @@ -58,7 +50,7 @@ export const buildAlertStatusFilter = (status: Status): Filter[] => { negate: false, disabled: false, type: 'phrase', - key: 'kibana.alert.workflow_status', + key: ALERT_WORKFLOW_STATUS, params: { query: status, }, @@ -76,7 +68,7 @@ export const buildAlertStatusesFilter = (statuses: Status[]): Filter[] => { bool: { should: statuses.map((status) => ({ term: { - 'kibana.alert.workflow_status': status, + [ALERT_WORKFLOW_STATUS]: status, }, })), }, @@ -94,8 +86,15 @@ export const buildAlertStatusesFilter = (statuses: Status[]): Filter[] => { ]; }; -export const buildAlertsRuleIdFilter = (ruleId: string | null): Filter[] => - ruleId +/** + * Builds Kuery filter for fetching alerts for a specific rule. Takes the rule's + * static id, i.e. `rule.ruleId` (not rule.id), so that alerts for _all + * historical instances_ of the rule are returned. + * + * @param ruleStaticId Rule's static id: `rule.ruleId` + */ +export const buildAlertsFilter = (ruleStaticId: string | null): Filter[] => + ruleStaticId ? [ { meta: { @@ -103,14 +102,14 @@ export const buildAlertsRuleIdFilter = (ruleId: string | null): Filter[] => negate: false, disabled: false, type: 'phrase', - key: 'kibana.alert.rule.uuid', + key: ALERT_RULE_RULE_ID, params: { - query: ruleId, + query: ruleStaticId, }, }, query: { match_phrase: { - 'kibana.alert.rule.uuid': ruleId, + [ALERT_RULE_RULE_ID]: ruleStaticId, }, }, }, @@ -127,10 +126,10 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): negate: true, disabled: false, type: 'exists', - key: 'kibana.alert.building_block_type', + key: ALERT_BUILDING_BLOCK_TYPE, value: 'exists', }, - query: { exists: { field: 'kibana.alert.building_block_type' } }, + query: { exists: { field: ALERT_BUILDING_BLOCK_TYPE } }, }, ]; @@ -183,121 +182,3 @@ export const requiredFieldsForActions = [ 'host.os.family', 'event.code', ]; - -// TODO: Once we are past experimental phase this code should be removed -export const buildAlertStatusFilterRuleRegistry = (status: Status): Filter[] => { - const combinedQuery = - status === 'acknowledged' - ? { - bool: { - should: [ - { - term: { - [ALERT_WORKFLOW_STATUS]: status, - }, - }, - { - term: { - [ALERT_WORKFLOW_STATUS]: 'in-progress', - }, - }, - ], - }, - } - : { - term: { - [ALERT_WORKFLOW_STATUS]: status, - }, - }; - - return [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: ALERT_WORKFLOW_STATUS, - params: { - query: status, - }, - }, - query: combinedQuery, - }, - ]; -}; - -// TODO: Once we are past experimental phase this code should be removed -export const buildAlertStatusesFilterRuleRegistry = (statuses: Status[]): Filter[] => { - const combinedQuery = { - bool: { - should: statuses.map((status) => ({ - term: { - [ALERT_WORKFLOW_STATUS]: status, - }, - })), - }, - }; - - return [ - { - meta: { - alias: null, - negate: false, - disabled: false, - }, - query: combinedQuery, - }, - ]; -}; - -export const buildShowBuildingBlockFilterRuleRegistry = ( - showBuildingBlockAlerts: boolean -): Filter[] => - showBuildingBlockAlerts - ? [] - : [ - { - meta: { - alias: null, - negate: true, - disabled: false, - type: 'exists', - key: 'kibana.alert.building_block_type', - value: 'exists', - }, - query: { exists: { field: 'kibana.alert.building_block_type' } }, - }, - ]; - -export const requiredFieldMappingsForActionsRuleRegistry = { - '@timestamp': '@timestamp', - 'event.kind': 'event.kind', - 'rule.severity': ALERT_RULE_SEVERITY, - 'rule.risk_score': ALERT_RULE_RISK_SCORE, - 'alert.uuid': ALERT_UUID, - 'alert.start': ALERT_START, - 'event.action': 'event.action', - 'alert.workflow_status': ALERT_WORKFLOW_STATUS, - 'alert.duration.us': ALERT_DURATION, - 'rule.uuid': ALERT_RULE_UUID, - 'rule.name': ALERT_RULE_NAME, - 'rule.category': ALERT_RULE_CATEGORY, - producer: ALERT_RULE_PRODUCER, - tags: 'tags', -}; - -export const alertsHeadersRuleRegistry: ColumnHeaderOptions[] = Object.entries( - requiredFieldMappingsForActionsRuleRegistry -).map(([alias, field]) => ({ - columnHeaderType: defaultColumnHeaderType, - displayAsText: alias, - id: field, -})); - -export const alertsDefaultModelRuleRegistry: SubsetTimelineModel = { - ...timelineDefaults, - columns: alertsHeadersRuleRegistry, - showCheckboxes: true, - excludedRowRendererIds: Object.values(RowRendererId), -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index bbab423738ca0..256a063c44158 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -40,9 +40,7 @@ import { updateAlertStatusAction } from './actions'; import { AditionalFiltersAction, AlertsUtilityBar } from './alerts_utility_bar'; import { alertsDefaultModel, - alertsDefaultModelRuleRegistry, buildAlertStatusFilter, - buildAlertStatusFilterRuleRegistry, requiredFieldsForActions, } from './default_config'; import { buildTimeRangeFilter } from './helpers'; @@ -106,8 +104,6 @@ export const AlertsTableComponent: React.FC = ({ const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - // TODO: Once we are past experimental phase this code should be removed - const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); const ACTION_BUTTON_COUNT = 4; const getGlobalQuery = useCallback( @@ -247,14 +243,9 @@ export const AlertsTableComponent: React.FC = ({ refetchQuery: inputsModel.Refetch, { status, selectedStatus }: UpdateAlertsStatusProps ) => { - // TODO: Once we are past experimental phase this code should be removed - const currentStatusFilter = ruleRegistryEnabled - ? buildAlertStatusFilterRuleRegistry(status) - : buildAlertStatusFilter(status); - await updateAlertStatusAction({ query: showClearSelectionAction - ? getGlobalQuery(currentStatusFilter)?.filterQuery + ? getGlobalQuery(buildAlertStatusFilter(status))?.filterQuery : undefined, alertIds: Object.keys(selectedEventIds), selectedStatus, @@ -273,7 +264,6 @@ export const AlertsTableComponent: React.FC = ({ showClearSelectionAction, onAlertStatusUpdateSuccess, onAlertStatusUpdateFailure, - ruleRegistryEnabled, ] ); @@ -327,24 +317,16 @@ export const AlertsTableComponent: React.FC = ({ ); const defaultFiltersMemo = useMemo(() => { - // TODO: Once we are past experimental phase this code should be removed - const alertStatusFilter = ruleRegistryEnabled - ? buildAlertStatusFilterRuleRegistry(filterGroup) - : buildAlertStatusFilter(filterGroup); + const alertStatusFilter = buildAlertStatusFilter(filterGroup); if (isEmpty(defaultFilters)) { return alertStatusFilter; } else if (defaultFilters != null && !isEmpty(defaultFilters)) { return [...defaultFilters, ...alertStatusFilter]; } - }, [defaultFilters, filterGroup, ruleRegistryEnabled]); + }, [defaultFilters, filterGroup]); const { filterManager } = useKibana().services.data.query; - // TODO: Once we are past experimental phase this code should be removed - const defaultTimelineModel = ruleRegistryEnabled - ? alertsDefaultModelRuleRegistry - : alertsDefaultModel; - const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { @@ -359,7 +341,7 @@ export const AlertsTableComponent: React.FC = ({ : c ), documentType: i18n.ALERTS_DOCUMENT_TYPE, - excludedRowRendererIds: defaultTimelineModel.excludedRowRendererIds as RowRendererId[], + excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds as RowRendererId[], filterManager, footerText: i18n.TOTAL_COUNT_OF_ALERTS, id: timelineId, @@ -370,7 +352,7 @@ export const AlertsTableComponent: React.FC = ({ showCheckboxes: true, }) ); - }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); + }, [dispatch, filterManager, tGridEnabled, timelineId]); const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); @@ -383,7 +365,7 @@ export const AlertsTableComponent: React.FC = ({ additionalFilters={additionalFiltersComponent} currentFilter={filterGroup} defaultCellActions={defaultCellActions} - defaultModel={defaultTimelineModel} + defaultModel={alertsDefaultModel} end={to} entityType="events" hasAlertsCrud={hasIndexWrite && hasIndexMaintenance} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx deleted file mode 100644 index 2e6991f87ec5a..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { TestProviders } from '../../../../common/mock'; -import { PreviewCustomQueryHistogram } from './custom_histogram'; - -jest.mock('../../../../common/containers/use_global_time'); - -describe('PreviewCustomQueryHistogram', () => { - const mockSetQuery = jest.fn(); - - beforeEach(() => { - (useGlobalTime as jest.Mock).mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: mockSetQuery, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders loader when isLoading is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); - }); - - test('it configures data and subtitle', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).props().data).toEqual( - [ - { - key: 'hits', - value: [ - { - g: 'All others', - x: 1602247050000, - y: 2314, - }, - { - g: 'All others', - x: 1602247162500, - y: 3471, - }, - { - g: 'All others', - x: 1602247275000, - y: 3369, - }, - ], - }, - ] - ); - }); - - test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { - const mockRefetch = jest.fn(); - - mount( - - - - ); - - expect(mockSetQuery).toHaveBeenCalledWith({ - id: 'queryPreviewCustomHistogramQuery', - inspect: { dsl: ['some dsl'], response: ['query response'] }, - loading: false, - refetch: mockRefetch, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx deleted file mode 100644 index 5392b08889128..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo } from 'react'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { getHistogramConfig } from '../rule_preview/helpers'; -import { - ChartSeriesConfigs, - ChartSeriesData, - ChartData, -} from '../../../../common/components/charts/common'; -import { InspectResponse } from '../../../../../public/types'; -import { inputsModel } from '../../../../common/store'; -import { PreviewHistogram } from './histogram'; - -export const ID = 'queryPreviewCustomHistogramQuery'; - -interface PreviewCustomQueryHistogramProps { - to: string; - from: string; - isLoading: boolean; - data: ChartData[]; - totalCount: number; - inspect: InspectResponse; - refetch: inputsModel.Refetch; -} - -export const PreviewCustomQueryHistogram = ({ - to, - from, - data, - totalCount, - inspect, - refetch, - isLoading, -}: PreviewCustomQueryHistogramProps) => { - const { setQuery, isInitializing } = useGlobalTime(); - - useEffect((): void => { - if (!isLoading && !isInitializing) { - setQuery({ id: ID, inspect, loading: isLoading, refetch }); - } - }, [setQuery, inspect, isLoading, isInitializing, refetch]); - - const barConfig = useMemo( - (): ChartSeriesConfigs => getHistogramConfig(to, from, true), - [from, to] - ); - - const subtitle = useMemo( - (): string => - isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount] - ); - - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx deleted file mode 100644 index df32223fc7ec3..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import * as i18n from '../rule_preview/translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { TestProviders } from '../../../../common/mock'; -import { PreviewEqlQueryHistogram } from './eql_histogram'; - -jest.mock('../../../../common/containers/use_global_time'); - -describe('PreviewEqlQueryHistogram', () => { - const mockSetQuery = jest.fn(); - - beforeEach(() => { - (useGlobalTime as jest.Mock).mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: mockSetQuery, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders loader when isLoading is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); - }); - - test('it configures data and subtitle', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') - ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([ - { - key: 'hits', - value: [ - { - g: 'All others', - x: 1602247050000, - y: 2314, - }, - { - g: 'All others', - x: 1602247162500, - y: 3471, - }, - { - g: 'All others', - x: 1602247275000, - y: 3369, - }, - ], - }, - ]); - }); - - test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { - const mockRefetch = jest.fn(); - - mount( - - - - ); - - expect(mockSetQuery).toHaveBeenCalledWith({ - id: 'queryEqlPreviewHistogramQuery', - inspect: { dsl: ['some dsl'], response: ['query response'] }, - loading: false, - refetch: mockRefetch, - }); - }); - - test('it displays histogram', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect( - wrapper.find('[data-test-subj="sharedPreviewQueryNoHistogramAvailable"]').exists() - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx deleted file mode 100644 index eae2a593d5f25..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo } from 'react'; - -import * as i18n from '../rule_preview/translations'; -import { getHistogramConfig } from '../rule_preview/helpers'; -import { - ChartSeriesData, - ChartSeriesConfigs, - ChartData, -} from '../../../../common/components/charts/common'; -import { InspectQuery } from '../../../../common/store/inputs/model'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { inputsModel } from '../../../../common/store'; -import { PreviewHistogram } from './histogram'; - -export const ID = 'queryEqlPreviewHistogramQuery'; - -interface PreviewEqlQueryHistogramProps { - to: string; - from: string; - totalCount: number; - isLoading: boolean; - data: ChartData[]; - inspect: InspectQuery; - refetch: inputsModel.Refetch; -} - -export const PreviewEqlQueryHistogram = ({ - from, - to, - totalCount, - data, - inspect, - refetch, - isLoading, -}: PreviewEqlQueryHistogramProps) => { - const { setQuery, isInitializing } = useGlobalTime(); - - useEffect((): void => { - if (!isInitializing) { - setQuery({ id: ID, inspect, loading: false, refetch }); - } - }, [setQuery, inspect, isInitializing, refetch]); - - const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]); - - const subtitle = useMemo( - (): string => - isLoading ? i18n.QUERY_PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), - [isLoading, totalCount] - ); - - const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); - - return ( - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx deleted file mode 100644 index 500a7f3d0e3db..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { TestProviders } from '../../../../common/mock'; -import { PreviewHistogram } from './histogram'; -import { getHistogramConfig } from '../rule_preview/helpers'; - -describe('PreviewHistogram', () => { - test('it renders loading icon if "isLoading" is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders chart if "isLoading" is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx deleted file mode 100644 index 3391ed1c5560a..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; -import styled from 'styled-components'; - -import { BarChart } from '../../../../common/components/charts/barchart'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; -import { ChartSeriesData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; - -const LoadingChart = styled(EuiLoadingChart)` - display: block; - margin: 0 auto; -`; - -interface PreviewHistogramProps { - id: string; - data: ChartSeriesData[]; - dataTestSubj?: string; - barConfig: ChartSeriesConfigs; - title: string; - subtitle: string; - disclaimer: string; - isLoading: boolean; -} - -export const PreviewHistogram = ({ - id, - data, - dataTestSubj, - barConfig, - title, - subtitle, - disclaimer, - isLoading, -}: PreviewHistogramProps) => { - return ( - <> - - - - - - - {isLoading ? ( - - ) : ( - - )} - - - <> - - -

{disclaimer}

-
- -
-
-
- - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx deleted file mode 100644 index f14bd5f7354d9..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ /dev/null @@ -1,502 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { of } from 'rxjs'; -import { ThemeProvider } from 'styled-components'; -import { mount } from 'enzyme'; - -import { TestProviders } from '../../../../common/mock'; -import { useKibana } from '../../../../common/lib/kibana'; -import { PreviewQuery } from './'; -import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_response.mock'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql/'; -import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; -import type { FilterMeta } from '@kbn/es-query'; - -const mockTheme = getMockTheme({ - eui: { - euiSuperDatePickerWidth: '180px', - }, -}); - -jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/containers/matrix_histogram'); -jest.mock('../../../../common/hooks/eql/'); - -describe('PreviewQuery', () => { - beforeEach(() => { - useKibana().services.notifications.toasts.addError = jest.fn(); - - useKibana().services.notifications.toasts.addWarning = jest.fn(); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 1, - refetch: jest.fn(), - data: [], - buckets: [], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - (useEqlPreview as jest.Mock).mockReturnValue([ - false, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - { - inspect: { dsl: [], response: [] }, - totalCount: 1, - refetch: jest.fn(), - data: [], - buckets: [], - }, - ]); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('it renders timeframe select and preview button on render', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="queryPreviewSelect"]').exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders preview button disabled if "isDisabled" is true', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeTruthy(); - }); - - test('it renders preview button disabled if "query" is undefined', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeTruthy(); - }); - - test('it renders preview button enabled if query exists', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - }); - - test('it renders preview button enabled if no query exists but filters do exist', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled - ).toBeFalsy(); - }); - - test('it renders query histogram when rule type is query and preview button clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 2, - refetch: jest.fn(), - data: [], - buckets: [], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders query histogram when rule type is saved_query and preview button clicked', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders eql histogram when preview button clicked and rule type is eql', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeTruthy(); - }); - - test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useEqlPreview as jest.Mock).mockReturnValue([ - false, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - { - inspect: { dsl: [], response: [] }, - totalCount: 2, - refetch: jest.fn(), - data: [], - buckets: [], - }, - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 500, - refetch: jest.fn(), - data: [], - buckets: [{ key: 'siem-kibana', doc_count: 500 }], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => { - const wrapper = mount( - - - - ); - - (useMatrixHistogram as jest.Mock).mockReturnValue([ - false, - { - inspect: { dsl: [], response: [] }, - totalCount: 500, - refetch: jest.fn(), - data: [], - buckets: [ - { key: 'siem-kibana', doc_count: 200 }, - { key: 'siem-windows', doc_count: 300 }, - ], - }, - (useKibana().services.data.search.search as jest.Mock).mockReturnValue( - of(getMockEqlResponse()) - ), - ]); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeTruthy(); - }); - - test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty array', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; - - expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); - }); - - test('it hides histogram when timeframe changes', () => { - const wrapper = mount( - - - - ); - - wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); - - wrapper - .find('[data-test-subj="queryPreviewTimeframeSelect"] select') - .at(0) - .simulate('change', { target: { value: 'd' } }); - - expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx deleted file mode 100644 index e7cc34ef49bef..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef } from 'react'; -import { Unit } from '@elastic/datemath'; -import styled from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSelect, - EuiFormRow, - EuiButton, - EuiCallOut, - EuiText, - EuiSpacer, -} from '@elastic/eui'; -import { debounce } from 'lodash/fp'; - -import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; -import * as i18n from '../rule_preview/translations'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy'; -import { FieldValueQueryBar } from '../query_bar'; -import { PreviewEqlQueryHistogram } from './eql_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql/'; -import { PreviewThresholdQueryHistogram } from './threshold_histogram'; -import { formatDate } from '../../../../common/components/super_date_picker'; -import { State, queryPreviewReducer } from './reducer'; -import { isNoisy } from '../rule_preview/helpers'; -import { PreviewCustomQueryHistogram } from './custom_histogram'; -import { FieldValueThreshold } from '../threshold_input'; - -const Select = styled(EuiSelect)` - width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth}; -`; - -const PreviewButton = styled(EuiButton)` - margin-left: 0; -`; - -export const initialState: State = { - timeframeOptions: [], - showHistogram: false, - timeframe: 'h', - warnings: [], - queryFilter: undefined, - toTime: '', - fromTime: '', - queryString: '', - language: 'kuery', - filters: [], - thresholdFieldExists: false, - showNonEqlHistogram: false, -}; - -export type Threshold = FieldValueThreshold | undefined; - -interface PreviewQueryProps { - dataTestSubj: string; - idAria: string; - query: FieldValueQueryBar | undefined; - index: string[]; - ruleType: Type; - threshold: Threshold; - isDisabled: boolean; -} - -export const PreviewQuery = ({ - ruleType, - dataTestSubj, - idAria, - query, - index, - threshold, - isDisabled, -}: PreviewQueryProps) => { - const [ - eqlQueryLoading, - startEql, - { - totalCount: eqlQueryTotal, - data: eqlQueryData, - refetch: eqlQueryRefetch, - inspect: eqlQueryInspect, - }, - ] = useEqlPreview(); - - const [ - { - thresholdFieldExists, - showNonEqlHistogram, - timeframeOptions, - showHistogram, - timeframe, - warnings, - queryFilter, - toTime, - fromTime, - queryString, - }, - dispatch, - ] = useReducer(queryPreviewReducer(), { - ...initialState, - toTime: formatDate('now-1h'), - fromTime: formatDate('now'), - }); - const [ - isMatrixHistogramLoading, - { inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets }, - startNonEql, - ] = useMatrixHistogram({ - errorMessage: i18n.QUERY_PREVIEW_ERROR, - endDate: fromTime, - startDate: toTime, - filterQuery: queryFilter, - indexNames: index, - includeMissingData: false, - histogramType: MatrixHistogramType.events, - stackByField: 'event.category', - threshold: ruleType === 'threshold' ? threshold : undefined, - skip: true, - }); - - const setQueryInfo = useCallback( - (queryBar: FieldValueQueryBar | undefined, indices: string[], type: Type): void => { - dispatch({ - type: 'setQueryInfo', - queryBar, - index: indices, - ruleType: type, - }); - }, - [dispatch] - ); - - const debouncedSetQueryInfo = useRef(debounce(500, setQueryInfo)); - - const setTimeframeSelect = useCallback( - (selection: Unit): void => { - dispatch({ - type: 'setTimeframeSelect', - timeframe: selection, - }); - }, - [dispatch] - ); - - const setRuleTypeChange = useCallback( - (type: Type): void => { - dispatch({ - type: 'setResetRuleTypeChange', - ruleType: type, - }); - }, - [dispatch] - ); - - const setWarnings = useCallback( - (yikes: string[]): void => { - dispatch({ - type: 'setWarnings', - warnings: yikes, - }); - }, - [dispatch] - ); - - const setNoiseWarning = useCallback((): void => { - dispatch({ - type: 'setNoiseWarning', - }); - }, [dispatch]); - - const setShowHistogram = useCallback( - (show: boolean): void => { - dispatch({ - type: 'setShowHistogram', - show, - }); - }, - [dispatch] - ); - - const setThresholdValues = useCallback( - (thresh: Threshold, type: Type): void => { - dispatch({ - type: 'setThresholdQueryVals', - threshold: thresh, - ruleType: type, - }); - }, - [dispatch] - ); - - useEffect(() => { - debouncedSetQueryInfo.current(query, index, ruleType); - }, [index, query, ruleType]); - - useEffect((): void => { - setThresholdValues(threshold, ruleType); - }, [setThresholdValues, threshold, ruleType]); - - useEffect((): void => { - setRuleTypeChange(ruleType); - }, [ruleType, setRuleTypeChange]); - - useEffect((): void => { - switch (ruleType) { - case 'eql': - if (isNoisy(eqlQueryTotal, timeframe)) { - setNoiseWarning(); - } - break; - case 'threshold': - const totalHits = thresholdFieldExists ? buckets.length : matrixHistTotal; - if (isNoisy(totalHits, timeframe)) { - setNoiseWarning(); - } - break; - default: - if (isNoisy(matrixHistTotal, timeframe)) { - setNoiseWarning(); - } - } - }, [ - timeframe, - matrixHistTotal, - eqlQueryTotal, - ruleType, - setNoiseWarning, - thresholdFieldExists, - buckets.length, - ]); - - const handlePreviewEqlQuery = useCallback( - (to: string, from: string): void => { - startEql({ - index, - query: queryString, - from, - to, - interval: timeframe, - }); - }, - [startEql, index, queryString, timeframe] - ); - - const handleSelectPreviewTimeframe = useCallback( - ({ target: { value } }: React.ChangeEvent): void => { - setTimeframeSelect(value as Unit); - }, - [setTimeframeSelect] - ); - - const handlePreviewClicked = useCallback((): void => { - const to = formatDate('now'); - const from = formatDate(`now-1${timeframe}`); - - setWarnings([]); - setShowHistogram(true); - - if (ruleType === 'eql') { - handlePreviewEqlQuery(to, from); - } else { - startNonEql(to, from); - } - }, [setWarnings, setShowHistogram, ruleType, handlePreviewEqlQuery, startNonEql, timeframe]); - - const previewButtonDisabled = useMemo(() => { - return ( - isMatrixHistogramLoading || - eqlQueryLoading || - isDisabled || - query == null || - (query != null && query.query.query === '' && query.filters.length === 0) - ); - }, [eqlQueryLoading, isDisabled, isMatrixHistogramLoading, query]); - - return ( - <> - - - -