diff --git a/.bazelignore b/.bazelignore index ac7a2d15a7aa4..ec61c2f3e7e75 100644 --- a/.bazelignore +++ b/.bazelignore @@ -8,7 +8,10 @@ .idea .teamcity .yarn-local-mirror -/bazel +bazel-bin +bazel-kibana +bazel-out +bazel-testlogs build node_modules target diff --git a/.bazelrc.common b/.bazelrc.common index fb8e8e86b9ef5..20a41c4cde9a0 100644 --- a/.bazelrc.common +++ b/.bazelrc.common @@ -18,17 +18,16 @@ build --disk_cache=~/.bazel-cache/disk-cache build --repository_cache=~/.bazel-cache/repository-cache # Bazel will create symlinks from the workspace directory to output artifacts. -# Build results will be placed in a directory called "bazel/bin" +# Build results will be placed in a directory called "bazel-bin" # This will still create a bazel-out symlink in # the project directory, which must be excluded from the # editor's search path. -build --symlink_prefix=bazel/ # To disable the symlinks altogether (including bazel-out) we can use # build --symlink_prefix=/ # however this makes it harder to find outputs. # Prevents the creation of bazel-out dir -build --experimental_no_product_name_out_symlink +# build --experimental_no_product_name_out_symlink # Make direct file system calls to create symlink trees build --experimental_inprocess_symlink_creation @@ -83,7 +82,7 @@ test:debug --test_output=streamed --test_strategy=exclusive --test_timeout=9999 run:debug --define=VERBOSE_LOGS=1 -- --node_options=--inspect-brk # The following option will change the build output of certain rules such as terser and may not be desirable in all cases # It will also output both the repo cache and action cache to a folder inside the repo -build:debug --compilation_mode=dbg --show_result=1 --disk_cache=bazel/disk-cache --repository_cache=bazel/repository-cache +build:debug --compilation_mode=dbg --show_result=1 # Turn off legacy external runfiles # This prevents accidentally depending on this feature, which Bazel will remove. diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index 5317b2c500b49..a63c2825816bd 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -2,8 +2,10 @@ set -e -# cache image used by kibana-load-testing project -docker pull "maven:3.6.3-openjdk-8-slim" +if [[ "$(which docker)" != "" && "$(command uname -m)" != "aarch64" ]]; then + # cache image used by kibana-load-testing project + docker pull "maven:3.6.3-openjdk-8-slim" +fi ./.ci/packer_cache_for_branch.sh master ./.ci/packer_cache_for_branch.sh 7.x diff --git a/.eslintignore b/.eslintignore index 4559711bb9dd3..4058d971b7642 100644 --- a/.eslintignore +++ b/.eslintignore @@ -21,19 +21,13 @@ snapshots.js # plugin overrides /src/core/lib/kbn_internal_native_observable -/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/plugins/vis_type_timelion/common/_generated_/** -/x-pack/legacy/plugins/**/__tests__/fixtures/** /x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook/build /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** -/x-pack/legacy/plugins/infra/common/graphql/types.ts -/x-pack/legacy/plugins/infra/public/graphql/types.ts -/x-pack/legacy/plugins/infra/server/graphql/types.ts -/x-pack/legacy/plugins/maps/public/vendor/** # package overrides /packages/elastic-eslint-config-kibana @@ -48,4 +42,4 @@ snapshots.js /packages/kbn-monaco/src/painless/antlr # Bazel -/bazel +/bazel-* diff --git a/.eslintrc.js b/.eslintrc.js index a7b45534391c0..19ba7cacc3c44 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -410,11 +410,7 @@ module.exports = { errorMessage: `Common code can not import from server or public, use a common directory.`, }, { - target: [ - 'src/legacy/**/*', - '(src|x-pack)/plugins/**/(public|server)/**/*', - 'examples/**/*', - ], + target: ['(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*'], from: [ 'src/core/public/**/*', '!src/core/public/index.ts', // relative import @@ -428,8 +424,6 @@ module.exports = { '!src/core/server/mocks{,.ts}', '!src/core/server/types{,.ts}', '!src/core/server/test_utils{,.ts}', - '!src/core/server/utils', // ts alias - '!src/core/server/utils/**/*', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 '!src/core/server/*.test.mocks{,.ts}', @@ -442,7 +436,6 @@ module.exports = { }, { target: [ - 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', '!(src|x-pack)/**/*.test.*', @@ -482,7 +475,7 @@ module.exports = { }, { target: ['src/core/**/*'], - from: ['plugins/**/*', 'src/plugins/**/*', 'src/legacy/ui/**/*'], + from: ['plugins/**/*', 'src/plugins/**/*'], errorMessage: 'The core cannot depend on any plugins.', }, { @@ -490,19 +483,6 @@ module.exports = { from: ['ui/**/*'], errorMessage: 'Plugins cannot import legacy UI code.', }, - { - from: ['src/legacy/ui/**/*', 'ui/**/*'], - target: [ - 'test/plugin_functional/plugins/**/public/np_ready/**/*', - 'test/plugin_functional/plugins/**/server/np_ready/**/*', - ], - allowSameFolder: true, - errorMessage: - 'NP-ready code should not import from /src/legacy/ui/** folder. ' + - 'Instead of importing from /src/legacy/ui/** deeply within a np_ready folder, ' + - 'import those things once at the top level of your plugin and pass those down, just ' + - 'like you pass down `core` and `plugins` objects.', - }, ], }, ], @@ -1180,7 +1160,7 @@ module.exports = { pathGroups: [ { pattern: - '{../../../../../../,../../../../../,../../../../,../../../,../../,../}{common/,*}__mocks__{*,/**}', + '{../../../../../../,../../../../../,../../../../,../../../,../../,../,./}{common/,*}__mocks__{*,/**}', group: 'unknown', }, { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f27885c1e32c3..a8dcafeb7753c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -107,7 +107,6 @@ /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation -#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-presentation #CC# /x-pack/plugins/dashboard_mode @elastic/kibana-presentation @@ -146,7 +145,6 @@ /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis -#CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis @@ -165,7 +163,6 @@ /packages/kbn-utils/ @elastic/kibana-operations /packages/kbn-cli-dev-mode/ @elastic/kibana-operations /src/cli/keystore/ @elastic/kibana-operations -/src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations @@ -202,9 +199,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/legacy/server/config/ @elastic/kibana-core -/src/legacy/server/http/ @elastic/kibana-core -/src/legacy/server/logging/ @elastic/kibana-core /src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core @@ -214,9 +208,6 @@ /src/plugins/kibana_overview/ @elastic/kibana-core /x-pack/plugins/global_search_bar/ @elastic/kibana-core #CC# /src/core/server/csp/ @elastic/kibana-core -#CC# /src/legacy/server/config/ @elastic/kibana-core -#CC# /src/legacy/server/http/ @elastic/kibana-core -#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-core #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core @@ -348,6 +339,7 @@ # Security Solution sub teams /x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/plugins/timelines @elastic/security-threat-hunting /x-pack/test/case_api_integration @elastic/security-threat-hunting /x-pack/plugins/lists @elastic/security-detections-response diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md index 86e321990d05f..67d2ee2d3286b 100644 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -2,7 +2,7 @@ name: 8.0 Breaking change about: Breaking changes from 7.x -> 8.0 title: "[Breaking change]" -labels: Team:Elasticsearch UI, Feature:Upgrade Assistant, Breaking Change +labels: Feature:Upgrade Assistant, Breaking Change assignees: '' --- @@ -12,8 +12,8 @@ assignees: '' ******* LABEL CHANGES NECESSARY ******** **************************************** -Please add a "NeededFor:${TeamName}" label to denote the team that is -requesting the breaking change to be surfaced in the Upgrade Assistant. +Please add a team label to denote the team that the +breaking change is applicable to. --> @@ -30,16 +30,14 @@ requesting the breaking change to be surfaced in the Upgrade Assistant. -**How can we programmatically determine whether the cluster is affected by this breaking change?** +**Can the change be registered with the [Kibana deprecation service](https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md)?** -**What can users do to address the change manually?** + - - -**How could we make migration easier with the Upgrade Assistant?** - - + **Are there any edge cases?** diff --git a/.github/relabel.yml b/.github/relabel.yml new file mode 100644 index 0000000000000..a737be356ce81 --- /dev/null +++ b/.github/relabel.yml @@ -0,0 +1,3 @@ +issues: + - missingLabel: needs-team + regex: ^(\:ml)|(Team:.*)$ \ No newline at end of file diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index d9d2d6d1ddb8b..37d04abda7530 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}], {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/.gitignore b/.gitignore index fbe28b8f1e77c..ce8fd38b18a92 100644 --- a/.gitignore +++ b/.gitignore @@ -75,5 +75,6 @@ report.asciidoc .yarn-local-mirror # Bazel -/bazel -/.bazelrc.user +bazel +bazel-* +.bazelrc.user diff --git a/.stylelintignore b/.stylelintignore index a48b3adfa3632..72d9d5104a0e9 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,3 +1,4 @@ x-pack/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss build target +bazel-* diff --git a/BUILD.bazel b/BUILD.bazel index 38a478565a4af..4502daeaacb59 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -2,6 +2,7 @@ # other packages builds and need to be included as inputs exports_files( [ + "tsconfig.base.json", "tsconfig.json", "package.json" ], diff --git a/docs/api/actions-and-connectors/legacy/create.asciidoc b/docs/api/actions-and-connectors/legacy/create.asciidoc index af4feddcb80fb..0361c4222986b 100644 --- a/docs/api/actions-and-connectors/legacy/create.asciidoc +++ b/docs/api/actions-and-connectors/legacy/create.asciidoc @@ -4,7 +4,7 @@ Legacy Create connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Creates a connector. diff --git a/docs/api/actions-and-connectors/legacy/delete.asciidoc b/docs/api/actions-and-connectors/legacy/delete.asciidoc index 170fceba2d157..9ec2c0d392a96 100644 --- a/docs/api/actions-and-connectors/legacy/delete.asciidoc +++ b/docs/api/actions-and-connectors/legacy/delete.asciidoc @@ -4,7 +4,7 @@ Legacy Delete connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Deletes a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/execute.asciidoc b/docs/api/actions-and-connectors/legacy/execute.asciidoc index 200844ab72f17..f01aa1585b192 100644 --- a/docs/api/actions-and-connectors/legacy/execute.asciidoc +++ b/docs/api/actions-and-connectors/legacy/execute.asciidoc @@ -4,7 +4,7 @@ Legacy Execute connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Executes a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/get.asciidoc b/docs/api/actions-and-connectors/legacy/get.asciidoc index 1b138fb7032e0..6413fce558f5b 100644 --- a/docs/api/actions-and-connectors/legacy/get.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get.asciidoc @@ -4,7 +4,7 @@ Legacy Get connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieves a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/get_all.asciidoc b/docs/api/actions-and-connectors/legacy/get_all.asciidoc index ba235955c005e..191eccb6f8d39 100644 --- a/docs/api/actions-and-connectors/legacy/get_all.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get_all.asciidoc @@ -4,7 +4,7 @@ Legacy Get all connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieves all connectors. diff --git a/docs/api/actions-and-connectors/legacy/list.asciidoc b/docs/api/actions-and-connectors/legacy/list.asciidoc index 8acfd5415af57..d78838dcbe974 100644 --- a/docs/api/actions-and-connectors/legacy/list.asciidoc +++ b/docs/api/actions-and-connectors/legacy/list.asciidoc @@ -4,7 +4,7 @@ Legacy List all connector types ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieves a list of all connector types. diff --git a/docs/api/actions-and-connectors/legacy/update.asciidoc b/docs/api/actions-and-connectors/legacy/update.asciidoc index 517daf9a40dca..6a33e765cf063 100644 --- a/docs/api/actions-and-connectors/legacy/update.asciidoc +++ b/docs/api/actions-and-connectors/legacy/update.asciidoc @@ -4,7 +4,7 @@ Legacy Update connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Updates the attributes for an existing connector. diff --git a/docs/api/alerting/legacy/create.asciidoc b/docs/api/alerting/legacy/create.asciidoc index 5c594d64a3f45..8363569541356 100644 --- a/docs/api/alerting/legacy/create.asciidoc +++ b/docs/api/alerting/legacy/create.asciidoc @@ -4,7 +4,7 @@ Legacy create alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Create {kib} alerts. diff --git a/docs/api/alerting/legacy/delete.asciidoc b/docs/api/alerting/legacy/delete.asciidoc index 68851973cab5b..2af420f2bc34e 100644 --- a/docs/api/alerting/legacy/delete.asciidoc +++ b/docs/api/alerting/legacy/delete.asciidoc @@ -4,7 +4,7 @@ Legacy delete alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Permanently remove an alert. diff --git a/docs/api/alerting/legacy/disable.asciidoc b/docs/api/alerting/legacy/disable.asciidoc index 56e06371570c2..1a9b928bfba78 100644 --- a/docs/api/alerting/legacy/disable.asciidoc +++ b/docs/api/alerting/legacy/disable.asciidoc @@ -4,7 +4,7 @@ Legacy disable alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Disable an alert. diff --git a/docs/api/alerting/legacy/enable.asciidoc b/docs/api/alerting/legacy/enable.asciidoc index 913d96a84352b..da4b466d6fda4 100644 --- a/docs/api/alerting/legacy/enable.asciidoc +++ b/docs/api/alerting/legacy/enable.asciidoc @@ -4,7 +4,7 @@ Legacy enable alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Enable an alert. diff --git a/docs/api/alerting/legacy/find.asciidoc b/docs/api/alerting/legacy/find.asciidoc index 94d9bc425bd21..7c493e9c8eb5b 100644 --- a/docs/api/alerting/legacy/find.asciidoc +++ b/docs/api/alerting/legacy/find.asciidoc @@ -4,7 +4,7 @@ Legacy find alerts ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve a paginated set of alerts based on condition. diff --git a/docs/api/alerting/legacy/get.asciidoc b/docs/api/alerting/legacy/get.asciidoc index f1014d18e8774..ee0f52f51005a 100644 --- a/docs/api/alerting/legacy/get.asciidoc +++ b/docs/api/alerting/legacy/get.asciidoc @@ -4,7 +4,7 @@ Legacy get alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve an alert by ID. diff --git a/docs/api/alerting/legacy/health.asciidoc b/docs/api/alerting/legacy/health.asciidoc index b25307fb5efd1..68f04cc715bd7 100644 --- a/docs/api/alerting/legacy/health.asciidoc +++ b/docs/api/alerting/legacy/health.asciidoc @@ -4,7 +4,7 @@ Legacy get Alerting framework health ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve the health status of the Alerting framework. diff --git a/docs/api/alerting/legacy/list.asciidoc b/docs/api/alerting/legacy/list.asciidoc index e9ef3bbc27cd9..be37be36cd0e8 100644 --- a/docs/api/alerting/legacy/list.asciidoc +++ b/docs/api/alerting/legacy/list.asciidoc @@ -4,7 +4,7 @@ Legacy list all alert types ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve a list of all alert types. diff --git a/docs/api/alerting/legacy/mute.asciidoc b/docs/api/alerting/legacy/mute.asciidoc index dff42f5911e53..cf7adc446a2fd 100644 --- a/docs/api/alerting/legacy/mute.asciidoc +++ b/docs/api/alerting/legacy/mute.asciidoc @@ -4,7 +4,7 @@ Legacy mute alert instance ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Mute an alert instance. diff --git a/docs/api/alerting/legacy/mute_all.asciidoc b/docs/api/alerting/legacy/mute_all.asciidoc index df89fa15d1590..bc865480340e2 100644 --- a/docs/api/alerting/legacy/mute_all.asciidoc +++ b/docs/api/alerting/legacy/mute_all.asciidoc @@ -4,7 +4,7 @@ Legacy mute all alert instances ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Mute all alert instances. diff --git a/docs/api/alerting/legacy/unmute.asciidoc b/docs/api/alerting/legacy/unmute.asciidoc index 0be7e40dc1a19..300cf71b57a01 100644 --- a/docs/api/alerting/legacy/unmute.asciidoc +++ b/docs/api/alerting/legacy/unmute.asciidoc @@ -4,7 +4,7 @@ Legacy unmute alert instance ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Unmute an alert instance. diff --git a/docs/api/alerting/legacy/unmute_all.asciidoc b/docs/api/alerting/legacy/unmute_all.asciidoc index 8687c2d2fe8bb..3b0a7afe5f44d 100644 --- a/docs/api/alerting/legacy/unmute_all.asciidoc +++ b/docs/api/alerting/legacy/unmute_all.asciidoc @@ -4,7 +4,7 @@ Legacy unmute all alert instances ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Unmute all alert instances. diff --git a/docs/api/alerting/legacy/update.asciidoc b/docs/api/alerting/legacy/update.asciidoc index bffdf26c31400..b9cce995660e6 100644 --- a/docs/api/alerting/legacy/update.asciidoc +++ b/docs/api/alerting/legacy/update.asciidoc @@ -4,7 +4,7 @@ Legacy update alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Update the attributes for an existing alert. diff --git a/docs/developer/architecture/core/application_service.asciidoc b/docs/developer/architecture/core/application_service.asciidoc new file mode 100644 index 0000000000000..ba3c6bbed72be --- /dev/null +++ b/docs/developer/architecture/core/application_service.asciidoc @@ -0,0 +1,40 @@ +[[application-service]] +== Application service +Kibana has migrated to be a Single Page Application. Plugins should use `Application service` API to instruct Kibana that an application should be loaded and rendered in the UI in response to user interactions. The service also provides utilities for controlling the navigation link state, seamlessly integrating routing between applications, and loading async chunks on demand. + +NOTE: The Application service is only available client side. + +[source,typescript] +---- +import { AppMountParameters, CoreSetup, Plugin, DEFAULT_APP_CATEGORIES } from 'kibana/public'; + +export class MyPlugin implements Plugin { + public setup(core: CoreSetup) { + core.application.register({ // <1> + category: DEFAULT_APP_CATEGORIES.kibana, + id: 'my-plugin', + title: 'my plugin title', + euiIconType: '/path/to/some.svg', + order: 100, + appRoute: '/app/my_plugin', // <2> + async mount(params: AppMountParameters) { // <3> + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services + const [coreStart, depsStart] = await core.getStartServices(); // <4> + // Render the application + return renderApp(coreStart, depsStart, params); // <5> + }, + }); + } +} +---- +<1> See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md[application.register interface] +<2> Application specific URL. +<3> `mount` callback is invoked when a user navigates to the application-specific URL. +<4> `core.getStartServices` method provides API available during `start` lifecycle. +<5> `mount` method must return a function that will be called to unmount the application, which is called when Kibana unmounts the application. You can put a clean-up logic there. + +NOTE: you are free to use any UI library to render a plugin application in DOM. +However, we recommend using React and https://elastic.github.io/eui[EUI] for all your basic UI +components to create a consistent UI experience. diff --git a/docs/developer/architecture/core/configuration-service.asciidoc b/docs/developer/architecture/core/configuration-service.asciidoc new file mode 100644 index 0000000000000..031135c7b790f --- /dev/null +++ b/docs/developer/architecture/core/configuration-service.asciidoc @@ -0,0 +1,149 @@ +[[configuration-service]] +== Configuration service +{kib} provides `ConfigService` for plugin developers that want to support +adjustable runtime behavior for their plugins. +Plugins can only read their own configuration values, it is not possible to access the configuration values from {kib} Core or other plugins directly. + +NOTE: The Configuration service is only available server side. + +[source,js] +---- +// in Legacy platform +const basePath = config.get('server.basePath'); +// in Kibana Platform 'basePath' belongs to the http service +const basePath = core.http.basePath.get(request); +---- + +To have access to your plugin config, you _should_: + +* Declare plugin-specific `configPath` (will fallback to plugin `id` +if not specified) in {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md[`kibana.json`] manifest file. +* Export schema validation for the config from plugin's main file. Schema is +mandatory. If a plugin reads from the config without schema declaration, +`ConfigService` will throw an error. + +*my_plugin/server/index.ts* +[source,typescript] +---- +import { schema, TypeOf } from '@kbn/config-schema'; +export const plugin = … +export const config = { + schema: schema.object(…), +}; +export type MyPluginConfigType = TypeOf; +---- + +* Read config value exposed via `PluginInitializerContext`: + +*my_plugin/server/index.ts* +[source,typescript] +---- +import type { PluginInitializerContext } from 'kibana/server'; +export class MyPlugin { + constructor(initializerContext: PluginInitializerContext) { + this.config$ = initializerContext.config.create(); + // or if config is optional: + this.config$ = initializerContext.config.createIfExists(); + } + ... +} +---- + +If your plugin also has a client-side part, you can also expose +configuration properties to it using the configuration `exposeToBrowser` +allow-list property. + +*my_plugin/server/index.ts* +[source,typescript] +---- +import { schema, TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + secret: schema.string({ defaultValue: 'Only on server' }), + uiProp: schema.string({ defaultValue: 'Accessible from client' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + uiProp: true, + }, + schema: configSchema, +}; +---- + +Configuration containing only the exposed properties will be then +available on the client-side using the plugin's `initializerContext`: + +*my_plugin/public/index.ts* +[source,typescript] +---- +interface ClientConfigType { + uiProp: string; +} + +export class MyPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + const config = this.initializerContext.config.get(); + } +---- + +All plugins are considered enabled by default. If you want to disable +your plugin, you could declare the `enabled` flag in the plugin +config. This is a special {kib} Platform key. {kib} reads its +value and won’t create a plugin instance if `enabled: false`. + +[source,js] +---- +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; +---- +[[handle-plugin-configuration-deprecations]] +=== Handle plugin configuration deprecations +If your plugin has deprecated configuration keys, you can describe them using +the `deprecations` config descriptor field. +Deprecations are managed on a per-plugin basis, meaning you don’t need to specify +the whole property path, but use the relative path from your plugin’s +configuration root. + +*my_plugin/server/index.ts* +[source,typescript] +---- +import { schema, TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + newProperty: schema.string({ defaultValue: 'Some string' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('oldProperty', 'newProperty'), + unused('someUnusedProperty'), + ], +}; +---- + +In some cases, accessing the whole configuration for deprecations is +necessary. For these edge cases, `renameFromRoot` and `unusedFromRoot` +are also accessible when declaring deprecations. + +*my_plugin/server/index.ts* +[source,typescript] +---- +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot, unusedFromRoot }) => [ + renameFromRoot('oldplugin.property', 'myplugin.property'), + unusedFromRoot('oldplugin.deprecated'), + ], +}; +---- diff --git a/docs/developer/architecture/core/elasticsearch-service.asciidoc b/docs/developer/architecture/core/elasticsearch-service.asciidoc new file mode 100644 index 0000000000000..55632c0117938 --- /dev/null +++ b/docs/developer/architecture/core/elasticsearch-service.asciidoc @@ -0,0 +1,30 @@ +[[elasticsearch-service]] +== Elasticsearch service +`Elasticsearch service` provides `elasticsearch.client` program API to communicate with Elasticsearch server HTTP API. + +NOTE: The Elasticsearch service is only available server side. You can use the {kib-repo}blob/{branch}/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md[Data plugin] APIs on the client side. + +`elasticsearch.client` interacts with Elasticsearch service on behalf of: + +- `kibana_system` user via `elasticsearch.client.asInternalUser.*` methods. +- a current end-user via `elasticsearch.client.asCurrentUser.*` methods. In this case Elasticsearch client should be given the current user credentials. +See <> and <>. + +{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md[Elasticsearch service API docs] + +[source,typescript] +---- +import { CoreStart, Plugin } from 'kibana/public'; + +export class MyPlugin implements Plugin { + public start(core: CoreStart) { + async function asyncTask() { + const result = await core.elasticsearch.client.asInternalUser.ping(…); + } + asyncTask(); + } +} +---- + +For advanced use-cases, such as a search, use {kib-repo}blob/{branch}/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md[Data plugin] + diff --git a/docs/developer/architecture/core/http-service.asciidoc b/docs/developer/architecture/core/http-service.asciidoc new file mode 100644 index 0000000000000..45468d618dd09 --- /dev/null +++ b/docs/developer/architecture/core/http-service.asciidoc @@ -0,0 +1,67 @@ +[[http-service]] +== HTTP service + +NOTE: The HTTP service is available both server and client side. + +=== Server side usage + +The server-side HttpService allows server-side plugins to register endpoints with built-in support for request validation. These endpoints may be used by client-side code or be exposed as a public API for users. Most plugins integrate directly with this service. + +The service allows plugins to: +* to extend the {kib} server with custom HTTP API. +* to execute custom logic on an incoming request or server response. +* implement custom authentication and authorization strategy. + +See {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md[HTTP service API docs] + +[source,typescript] +---- +import { schema } from '@kbn/config-schema'; +import type { CoreSetup, Plugin } from 'kibana/server'; + +export class MyPlugin implements Plugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + + const validate = { + params: schema.object({ + id: schema.string(), + }), + }; + + router.get({ + path: 'my_plugin/{id}', + validate + }, + async (context, request, response) => { + const data = await findObject(request.params.id); + if (!data) return response.notFound(); + return response.ok({ + body: data, + headers: { + 'content-type': 'application/json' + } + }); + }); + } +} +---- + +=== Client side usage + +The HTTP service is also offered on the client side and provides an API to communicate with the {kib} server via HTTP interface. +The client-side HttpService is a preconfigured wrapper around `window.fetch` that includes some default behavior and automatically handles common errors (such as session expiration). The service should only be used for access to backend endpoints registered by the same plugin. Feel free to use another HTTP client library to request 3rd party services. + +[source,typescript] +---- +import { CoreStart } from 'kibana/public'; +interface ResponseType {…}; +interface MyPluginData {…}; +async function fetchData(core: CoreStart) { + return await core.http.get( + '/api/my_plugin/', + { query: … }, + ); +} +---- +See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.httpsetup.md[for all available API]. diff --git a/docs/developer/architecture/core/index.asciidoc b/docs/developer/architecture/core/index.asciidoc index 4a86c90cf8c10..53720a593d3f2 100644 --- a/docs/developer/architecture/core/index.asciidoc +++ b/docs/developer/architecture/core/index.asciidoc @@ -27,421 +27,18 @@ export class MyPlugin { } ---- -=== Server-side -[[configuration-service]] -==== Configuration service -{kib} provides `ConfigService` if a plugin developer may want to support -adjustable runtime behavior for their plugins. -Plugins can only read their own configuration values, it is not possible to access the configuration values from {kib} Core or other plugins directly. +The services that core provides are: -[source,js] ----- -// in Legacy platform -const basePath = config.get('server.basePath'); -// in Kibana Platform 'basePath' belongs to the http service -const basePath = core.http.basePath.get(request); ----- - -To have access to your plugin config, you _should_: +* <> +* <> +* <> +* <> +* <> +* <> +* <> -* Declare plugin-specific `configPath` (will fallback to plugin `id` -if not specified) in {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md[`kibana.json`] manifest file. -* Export schema validation for the config from plugin's main file. Schema is -mandatory. If a plugin reads from the config without schema declaration, -`ConfigService` will throw an error. - -*my_plugin/server/index.ts* -[source,typescript] ----- -import { schema, TypeOf } from '@kbn/config-schema'; -export const plugin = … -export const config = { - schema: schema.object(…), -}; -export type MyPluginConfigType = TypeOf; ----- - -* Read config value exposed via `PluginInitializerContext`. -*my_plugin/server/index.ts* -[source,typescript] ----- -import type { PluginInitializerContext } from 'kibana/server'; -export class MyPlugin { - constructor(initializerContext: PluginInitializerContext) { - this.config$ = initializerContext.config.create(); - // or if config is optional: - this.config$ = initializerContext.config.createIfExists(); - } ----- - -If your plugin also has a client-side part, you can also expose -configuration properties to it using the configuration `exposeToBrowser` -allow-list property. - -*my_plugin/server/index.ts* -[source,typescript] ----- -import { schema, TypeOf } from '@kbn/config-schema'; -import type { PluginConfigDescriptor } from 'kibana/server'; -const configSchema = schema.object({ - secret: schema.string({ defaultValue: 'Only on server' }), - uiProp: schema.string({ defaultValue: 'Accessible from client' }), -}); -type ConfigType = TypeOf; -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - uiProp: true, - }, - schema: configSchema, -}; ----- - -Configuration containing only the exposed properties will be then -available on the client-side using the plugin's `initializerContext`: - -*my_plugin/public/index.ts* -[source,typescript] ----- -interface ClientConfigType { - uiProp: string; -} - -export class MyPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup(core: CoreSetup, deps: {}) { - const config = this.initializerContext.config.get(); - } ----- - -All plugins are considered enabled by default. If you want to disable -your plugin, you could declare the `enabled` flag in the plugin -config. This is a special {kib} Platform key. {kib} reads its -value and won’t create a plugin instance if `enabled: false`. - -[source,js] ----- -export const config = { - schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), -}; ----- -[[handle-plugin-configuration-deprecations]] -===== Handle plugin configuration deprecations -If your plugin has deprecated configuration keys, you can describe them using -the `deprecations` config descriptor field. -Deprecations are managed on a per-plugin basis, meaning you don’t need to specify -the whole property path, but use the relative path from your plugin’s -configuration root. - -*my_plugin/server/index.ts* -[source,typescript] ----- -import { schema, TypeOf } from '@kbn/config-schema'; -import type { PluginConfigDescriptor } from 'kibana/server'; - -const configSchema = schema.object({ - newProperty: schema.string({ defaultValue: 'Some string' }), -}); - -type ConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: ({ rename, unused }) => [ - rename('oldProperty', 'newProperty'), - unused('someUnusedProperty'), - ], -}; ----- - -In some cases, accessing the whole configuration for deprecations is -necessary. For these edge cases, `renameFromRoot` and `unusedFromRoot` -are also accessible when declaring deprecations. - -*my_plugin/server/index.ts* -[source,typescript] ----- -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: ({ renameFromRoot, unusedFromRoot }) => [ - renameFromRoot('oldplugin.property', 'myplugin.property'), - unusedFromRoot('oldplugin.deprecated'), - ], -}; ----- -==== Logging service -Allows a plugin to provide status and diagnostic information. -For detailed instructions see the {kib-repo}blob/{branch}/src/core/server/logging/README.md[logging service documentation]. - -[source,typescript] ----- -import type { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; - -export class MyPlugin implements Plugin { - private readonly logger: Logger; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get(); - } - - public setup(core: CoreSetup) { - try { - this.logger.debug('doing something...'); - // … - } catch (e) { - this.logger.error('failed doing something...'); - } - } -} ----- - -==== Elasticsearch service -`Elasticsearch service` provides `elasticsearch.client` program API to communicate with Elasticsearch server REST API. -`elasticsearch.client` interacts with Elasticsearch service on behalf of: - -- `kibana_system` user via `elasticsearch.client.asInternalUser.*` methods. -- a current end-user via `elasticsearch.client.asCurrentUser.*` methods. In this case Elasticsearch client should be given the current user credentials. -See <> and <>. - -{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.elasticsearchservicestart.md[Elasticsearch service API docs] - -[source,typescript] ----- -import { CoreStart, Plugin } from 'kibana/public'; - -export class MyPlugin implements Plugin { - public start(core: CoreStart) { - async function asyncTask() { - const result = await core.elasticsearch.client.asInternalUser.ping(…); - } - asyncTask(); - } -} ----- -For advanced use-cases, such as a search, use {kib-repo}blob/{branch}/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md[Data plugin] -include::saved-objects-service.asciidoc[leveloffset=+1] - -==== HTTP service -Allows plugins: - -* to extend the {kib} server with custom REST API. -* to execute custom logic on an incoming request or server response. -* implement custom authentication and authorization strategy. - -See {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.md[HTTP service API docs] - -[source,typescript] ----- -import { schema } from '@kbn/config-schema'; -import type { CoreSetup, Plugin } from 'kibana/server'; - -export class MyPlugin implements Plugin { - public setup(core: CoreSetup) { - const router = core.http.createRouter(); - - const validate = { - params: schema.object({ - id: schema.string(), - }), - }; - - router.get({ - path: 'my_plugin/{id}', - validate - }, - async (context, request, response) => { - const data = await findObject(request.params.id); - if (!data) return response.notFound(); - return response.ok({ - body: data, - headers: { - 'content-type': 'application/json' - } - }); - }); - } -} ----- - -==== UI settings service -The program interface to <>. -It makes it possible for Kibana plugins to extend Kibana UI Settings Management with custom settings. - -See: - -- {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md[UI settings service Setup API docs] -- {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.uisettingsservicestart.register.md[UI settings service Start API docs] - -[source,typescript] ----- -import { schema } from '@kbn/config-schema'; -import type { CoreSetup,Plugin } from 'kibana/server'; - -export class MyPlugin implements Plugin { - public setup(core: CoreSetup) { - core.uiSettings.register({ - custom: { - value: '42', - schema: schema.string(), - }, - }); - const router = core.http.createRouter(); - router.get({ - path: 'my_plugin/{id}', - validate: …, - }, - async (context, request, response) => { - const customSetting = await context.uiSettings.client.get('custom'); - … - }); - } -} - ----- - -=== Client-side -==== Application service -Kibana has migrated to be a Single Page Application. Plugins should use `Application service` API to instruct Kibana what an application should be loaded & rendered in the UI in response to user interactions. -[source,typescript] ----- -import { AppMountParameters, CoreSetup, Plugin, DEFAULT_APP_CATEGORIES } from 'kibana/public'; - -export class MyPlugin implements Plugin { - public setup(core: CoreSetup) { - core.application.register({ // <1> - category: DEFAULT_APP_CATEGORIES.kibana, - id: 'my-plugin', - title: 'my plugin title', - euiIconType: '/path/to/some.svg', - order: 100, - appRoute: '/app/my_plugin', // <2> - async mount(params: AppMountParameters) { // <3> - // Load application bundle - const { renderApp } = await import('./application'); - // Get start services - const [coreStart, depsStart] = await core.getStartServices(); // <4> - // Render the application - return renderApp(coreStart, depsStart, params); // <5> - }, - }); - } -} ----- -<1> See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.applicationsetup.register.md[application.register interface] -<2> Application specific URL. -<3> `mount` callback is invoked when a user navigates to the application-specific URL. -<4> `core.getStartServices` method provides API available during `start` lifecycle. -<5> `mount` method must return a function that will be called to unmount the application. - -NOTE:: you are free to use any UI library to render a plugin application in DOM. -However, we recommend using React and https://elastic.github.io/eui[EUI] for all your basic UI -components to create a consistent UI experience. - -==== HTTP service -Provides API to communicate with the {kib} server. Feel free to use another HTTP client library to request 3rd party services. - -[source,typescript] ----- -import { CoreStart } from 'kibana/public'; -interface ResponseType {…}; -async function fetchData(core: CoreStart) { - return await core.http.get<>( - '/api/my_plugin/', - { query: … }, - ); -} ----- -See {kib-repo}blob/{branch}/docs/development/core/public/kibana-plugin-core-public.httpsetup.md[for all available API]. - -==== Elasticsearch service -Not available in the browser. Use {kib-repo}blob/{branch}/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md[Data plugin] instead. - -== Patterns -[[scoped-services]] -=== Scoped services -Whenever Kibana needs to get access to data saved in elasticsearch, it -should perform a check whether an end-user has access to the data. In -the legacy platform, Kibana requires binding elasticsearch related API -with an incoming request to access elasticsearch service on behalf of a -user. - -[source,js] ----- -async function handler(req, res) { - const dataCluster = server.plugins.elasticsearch.getCluster('data'); - const data = await dataCluster.callWithRequest(req, 'ping'); -} ----- - -The Kibana Platform introduced a handler interface on the server-side to perform that association -internally. Core services, that require impersonation with an incoming -request, are exposed via `context` argument of -{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandler.md[the -request handler interface.] The above example looks in the Kibana Platform -as - -[source,js] ----- -async function handler(context, req, res) { - const data = await context.core.elasticsearch.client.asCurrentUser('ping'); -} ----- - -The -{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md[request -handler context] exposed the next scoped *core* services: - -[width="100%",cols="30%,70%",options="header",] -|=== -|Legacy Platform |Kibana Platform -|`request.getSavedObjectsClient` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md[`context.savedObjects.client`] - -|`server.plugins.elasticsearch.getCluster('admin')` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md[`context.elasticsearch.client`] - -|`server.plugins.elasticsearch.getCluster('data')` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md[`context.elasticsearch.client`] - -|`request.getUiSettingsService` -|{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md[`context.uiSettings.client`] -|=== - -==== Declare a custom scoped service - -Plugins can extend the handler context with a custom API that will be -available to the plugin itself and all dependent plugins. For example, -the plugin creates a custom elasticsearch client and wants to use it via -the request handler context: - -[source,typescript] ----- -import type { CoreSetup, RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; - -interface MyRequestHandlerContext extends RequestHandlerContext { - myPlugin: { - client: IScopedClusterClient; - }; -} - -class MyPlugin { - setup(core: CoreSetup) { - const client = core.elasticsearch.createClient('myClient'); - core.http.registerRouteHandlerContext('myPlugin', (context, req, res) => { - return { client: client.asScoped(req) }; - }); - const router = core.http.createRouter(); - router.get( - { path: '/api/my-plugin/', validate: … }, - async (context, req, res) => { - // context type is inferred as MyPluginContext - const data = await context.myPlugin.client.asCurrentUser('endpoint'); - } - ); - } ----- diff --git a/docs/developer/architecture/core/logging-configuration-migration.asciidoc b/docs/developer/architecture/core/logging-configuration-migration.asciidoc new file mode 100644 index 0000000000000..19f10a881d5e8 --- /dev/null +++ b/docs/developer/architecture/core/logging-configuration-migration.asciidoc @@ -0,0 +1,84 @@ +[[logging-configuration-migration]] +== Logging configuration migration + +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write +root appenders, make sure that it contains `default` appender to provide backward compatibility. + +NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats. +These will be removed when the `default` appender is no longer required. If you define an appender for a logger, +the log messages aren't handled by the `root` logger anymore and are not forwarded to the legacy logging service. + +[[logging-pattern-format-old-and-new-example]] +[options="header"] +|=== + +| Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format + +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | Absolute `23:33:22.011` + +| logger | `parent.child` | `['parent', 'child']` + +| level | `DEBUG` | `['debug']` + +| meta | stringified JSON object `{"to": "v8"}`| N/A + +| pid | can be configured as `%pid` | N/A + +|=== + +[[logging-json-format-old-and-new-example]] +[options="header"] +|=== + +| Parameter | Platform log record in **json** format | Legacy Platform log record **json** format + +| @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | ISO8601 `2012-01-31T23:33:22.011Z` + +| logger | `log.logger: parent.child` | `tags: ['parent', 'child']` + +| level | `log.level: DEBUG` | `tags: ['debug']` + +| meta | merged in log record `{... "to": "v8"}` | merged in log record `{... "to": "v8"}` + +| pid | `process.pid: 12345` | `pid: 12345` + +| type | N/A | `type: log` + +| error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` + +|=== + +[[logging-cli-migration]] +=== Logging configuration via CLI + +As is the case for any of Kibana's config settings, you can specify your logging configuration via the CLI. For convenience, the `--verbose` and `--silent` flags exist as shortcuts and will continue to be supported beyond v7. + +If you wish to override these flags, you can always do so by passing your preferred logging configuration directly to the CLI. For example, with the following configuration: + +[source,yaml] +---- +logging: + appenders: + custom: + type: console + layout: + type: pattern + pattern: "[%date][%level] %message" +---- + +you can override the flags with: + +[options="header"] +|=== + +| legacy logging | {kib} Platform logging | cli shortcuts + +|--verbose| --logging.root.level=debug --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | --verbose + +|--quiet| --logging.root.level=error --logging.root.appenders[0]=default --logging.root.appenders[1]=custom | not supported + +|--silent| --logging.root.level=off | --silent +|=== + +NOTE: To preserve backwards compatibility, you are required to pass the root `default` appender until the legacy logging system is removed in `v8.0`. diff --git a/docs/developer/architecture/core/logging-service.asciidoc b/docs/developer/architecture/core/logging-service.asciidoc new file mode 100644 index 0000000000000..7dc2a4ca1f4ce --- /dev/null +++ b/docs/developer/architecture/core/logging-service.asciidoc @@ -0,0 +1,545 @@ +[[logging-service]] +== Logging service +Allows a plugin to provide status and diagnostic information. + +NOTE: The Logging service is only available server side. + +[source,typescript] +---- +import type { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; + +export class MyPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + try { + this.logger.debug('doing something...'); + // … + } catch (e) { + this.logger.error('failed doing something...'); + } + } +} +---- + +The way logging works in {kib} is inspired by the `log4j 2` logging framework used by {ref-bare}/current/logging.html[Elasticsearch]. +The main idea is to have consistent logging behavior (configuration, log format etc.) across the entire Elastic Stack where possible. + +=== Loggers, Appenders and Layouts + +The {kib} logging system has three main components: _loggers_, _appenders_ and _layouts_. These components allow us to log +messages according to message type and level, to control how these messages are formatted and where the final logs +will be displayed or stored. + +__Loggers__ define what logging settings should be applied to a particular logger. + +__<>__ define where log messages are displayed (eg. stdout or console) and stored (eg. file on the disk). + +__<>__ define how log messages are formatted and what type of information they include. + +[[log-level]] +=== Log level + +Currently we support the following log levels: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. + +Levels are ordered, so _all_ > _fatal_ > _error_ > _warn_ > _info_ > _debug_ > _trace_ > _off_. + +A log record is being logged by the logger if its level is higher than or equal to the level of its logger. Otherwise, +the log record is ignored. + +The _all_ and _off_ levels can be used only in configuration and are just handy shortcuts that allow you to log every +log record or disable logging entirely or for a specific logger. These levels are also configurable as <>. + +[[logging-layouts]] +=== Layouts + +Every appender should know exactly how to format log messages before they are written to the console or file on the disk. +This behavior is controlled by the layouts and configured through `appender.layout` configuration property for every +custom appender. Currently we don't define any default layout for the +custom appenders, so one should always make the choice explicitly. + +There are two types of layout supported at the moment: <> and <>. + +[[pattern-layout]] +==== Pattern layout + +With `pattern` layout it's possible to define a string pattern with special placeholders `%conversion_pattern` that will be replaced with data from the actual log message. By default the following pattern is used: `[%date][%level][%logger] %message`. + +NOTE: The `pattern` layout uses a sub-set of https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout[log4j 2 pattern syntax] and **doesn't implement** all `log4j 2` capabilities. + +The conversions that are provided out of the box are: + +**level** +Outputs the <> of the logging event. +Example of `%level` output: `TRACE`, `DEBUG`, `INFO`. + +**logger** +Outputs the name of the logger that published the logging event. +Example of `%logger` output: `server`, `server.http`, `server.http.kibana`. + +**message** +Outputs the application supplied message associated with the logging event. + +**meta** +Outputs the entries of `meta` object data in **json** format, if one is present in the event. +Example of `%meta` output: +[source,bash] +---- +// Meta{from: 'v7', to: 'v8'} +'{"from":"v7","to":"v8"}' +// Meta empty object +'{}' +// no Meta provided +'' +---- + +[[date-format]] +**date** +Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. +Timezone name is expected to be one from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[TZ database name]. +Timezone defaults to the host timezone when not explicitly specified. +Example of `%date` output: + +[[date-conversion-pattern-examples]] +[options="header"] +|=== + +| Conversion pattern | Example + +| `%date` +| `2012-02-01T14:30:22.011Z` uses `ISO8601` format by default + +| `%date{ISO8601}` +| `2012-02-01T14:30:22.011Z` + +| `%date{ISO8601_TZ}` +| `2012-02-01T09:30:22.011-05:00` `ISO8601` with timezone + +| `%date{ISO8601_TZ}{America/Los_Angeles}` +| `2012-02-01T06:30:22.011-08:00` + +| `%date{ABSOLUTE}` +| `09:30:22.011` + +| `%date{ABSOLUTE}{America/Los_Angeles}` +| `06:30:22.011` + +| `%date{UNIX}` +| `1328106622` + +| `%date{UNIX_MILLIS}` +| `1328106622011` + +|=== + +**pid** +Outputs the process ID. + +The pattern layout also offers a `highlight` option that allows you to highlight +some parts of the log message with different colors. Highlighting is quite handy if log messages are forwarded +to a terminal with color support. + +[[json-layout]] +==== JSON layout +With `json` layout log messages will be formatted as JSON strings in https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[ECS format] that includes a timestamp, log level, logger, message text and any other metadata that may be associated with the log message itself. + +[[logging-appenders]] +=== Appenders + +[[rolling-file-appender]] +==== Rolling File Appender + +Similar to Log4j's `RollingFileAppender`, this appender will log into a file, and rotate it following a rolling +strategy when the configured policy triggers. + +===== Triggering Policies + +The triggering policy determines when a rollover should occur. + +There are currently two policies supported: `size-limit` and `time-interval`. + +[[size-limit-triggering-policy]] +**SizeLimitTriggeringPolicy** + +This policy will rotate the file when it reaches a predetermined size. + +[source,yaml] +---- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + type: size-limit + size: 50mb + strategy: + //... + layout: + type: pattern +---- + +The options are: + +- `size` + +The maximum size the log file should reach before a rollover should be performed. The default value is `100mb` + +[[time-interval-triggering-policy]] +**TimeIntervalTriggeringPolicy** + +This policy will rotate the file every given interval of time. + +[source,yaml] +---- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + type: time-interval + interval: 10s + modulate: true + strategy: + //... + layout: + type: pattern +---- + +The options are: + +- `interval` + +How often a rollover should occur. The default value is `24h` + +- `modulate` + +Whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. + +For example, if modulate is true and the interval is `4h`, if the current hour is 3 am then the first rollover will occur at 4 am +and then next ones will occur at 8 am, noon, 4pm, etc. The default value is `true`. + +===== Rolling strategies + +The rolling strategy determines how the rollover should occur: both the naming of the rolled files, +and their retention policy. + +There is currently one strategy supported: `numeric`. + +**NumericRollingStrategy** + +This strategy will suffix the file with a given pattern when rolling, +and will retains a fixed amount of rolled files. + +[source,yaml] +---- +logging: + appenders: + rolling-file: + type: rolling-file + fileName: /var/logs/kibana.log + policy: + // ... + strategy: + type: numeric + pattern: '-%i' + max: 2 + layout: + type: pattern +---- + +For example, with this configuration: + +- During the first rollover kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts + being written to. +- During the second rollover kibana-1.log is renamed to kibana-2.log and kibana.log is renamed to kibana-1.log. + A new kibana.log file is created and starts being written to. +- During the third and subsequent rollovers, kibana-2.log is deleted, kibana-1.log is renamed to kibana-2.log and + kibana.log is renamed to kibana-1.log. A new kibana.log file is created and starts being written to. + +The options are: + +- `pattern` + +The suffix to append to the file path when rolling. Must include `%i`, as this is the value +that will be converted to the file index. + +For example, with `fileName: /var/logs/kibana.log` and `pattern: '-%i'`, the rolling files created +will be `/var/logs/kibana-1.log`, `/var/logs/kibana-2.log`, and so on. The default value is `-%i` + +- `max` + +The maximum number of files to keep. Once this number is reached, oldest files will be deleted. The default value is `7` + +==== Rewrite Appender + +WARNING: This appender is currently considered experimental and is not intended +for public consumption. The API is subject to change at any time. + +Similar to log4j's `RewriteAppender`, this appender serves as a sort of middleware, +modifying the provided log events before passing them along to another +appender. + +[source,yaml] +---- +logging: + appenders: + my-rewrite-appender: + type: rewrite + appenders: [console, file] # name of "destination" appender(s) + policy: + # ... +---- + +The most common use case for the `RewriteAppender` is when you want to +filter or censor sensitive data that may be contained in a log entry. +In fact, with a default configuration, {kib} will automatically redact +any `authorization`, `cookie`, or `set-cookie` headers when logging http +requests & responses. + +To configure additional rewrite rules, you'll need to specify a <>. + +[[rewrite-policies]] +===== Rewrite Policies + +Rewrite policies exist to indicate which parts of a log record can be +modified within the rewrite appender. + +**Meta** + +The `meta` rewrite policy can read and modify any data contained in the +`LogMeta` before passing it along to a destination appender. + +Meta policies must specify one of three modes, which indicate which action +to perform on the configured properties: +- `update` updates an existing property at the provided `path`. +- `remove` removes an existing property at the provided `path`. + +The `properties` are listed as a `path` and `value` pair, where `path` is +the dot-delimited path to the target property in the `LogMeta` object, and +`value` is the value to add or update in that target property. When using +the `remove` mode, a `value` is not necessary. + +Here's an example of how you would replace any `cookie` header values with `[REDACTED]`: + +[source,yaml] +---- +logging: + appenders: + my-rewrite-appender: + type: rewrite + appenders: [console] + policy: + type: meta # indicates that we want to rewrite the LogMeta + mode: update # will update an existing property only + properties: + - path: "http.request.headers.cookie" # path to property + value: "[REDACTED]" # value to replace at path +---- + +Rewrite appenders can even be passed to other rewrite appenders to apply +multiple filter policies/modes, as long as it doesn't create a circular +reference. Each rewrite appender is applied sequentially (one after the other). + +[source,yaml] +---- +logging: + appenders: + remove-request-headers: + type: rewrite + appenders: [censor-response-headers] # redirect to the next rewrite appender + policy: + type: meta + mode: remove + properties: + - path: "http.request.headers" # remove all request headers + censor-response-headers: + type: rewrite + appenders: [console] # output to console + policy: + type: meta + mode: update + properties: + - path: "http.response.headers.set-cookie" + value: "[REDACTED]" +---- + +===== Complete Example For Rewrite Appender + +[source,yaml] +---- +logging: + appenders: + custom_console: + type: console + layout: + type: pattern + highlight: true + pattern: "[%date][%level][%logger] %message %meta" + file: + type: file + fileName: ./kibana.log + layout: + type: json + censor: + type: rewrite + appenders: [custom_console, file] + policy: + type: meta + mode: update + properties: + - path: "http.request.headers.cookie" + value: "[REDACTED]" + loggers: + - name: http.server.response + appenders: [censor] # pass these logs to our rewrite appender + level: debug +---- + +[[logger-hierarchy]] +=== Logger hierarchy + +Every logger has a unique name that follows a hierarchical naming rule. The logger is considered to be an +ancestor of another logger if its name followed by a `.` is a prefix of the descendant logger. For example, a logger +named `a.b` is an ancestor of logger `a.b.c`. All top-level loggers are descendants of a special `root` logger at the top of the logger hierarchy. The `root` logger always exists and +fully configured. + +You can configure _<>_ and _appenders_ for a specific logger. If a logger only has a _log level_ configured, then the _appenders_ configuration applied to the logger is inherited from the ancestor logger. + +NOTE: In the current implementation we __don't support__ so called _appender additivity_ when log messages are forwarded to _every_ distinct appender within the +ancestor chain including `root`. That means that log messages are only forwarded to appenders that are configured for a particular logger. If a logger doesn't have any appenders configured, the configuration of that particular logger will be inherited from its closest ancestor. + +[[dedicated-loggers]] +==== Dedicated loggers + +**Root** + +The `root` logger has a dedicated configuration node since this logger is special and should always exist. By default `root` is configured with `info` level and `default` appender that is also always available. This is the configuration that all custom loggers will use unless they're re-configured explicitly. + +For example to see _all_ log messages that fall back on the `root` logger configuration, just add one line to the configuration: + +[source,yaml] +---- +logging.root.level: all +---- + +Or disable logging entirely with `off`: + +[source,yaml] +---- +logging.root.level: off +---- + +**Metrics Logs** + +The `metrics.ops` logger is configured with `debug` level and will automatically output sample system and process information at a regular interval. +The metrics that are logged are a subset of the data collected and are formatted in the log message as follows: + +[options="header"] +|=== + +| Ops formatted log property | Location in metrics service | Log units + +| memory | process.memory.heap.used_in_bytes | http://numeraljs.com/#format[depends on the value], typically MB or GB + +| uptime | process.uptime_in_millis | HH:mm:ss + +| load | os.load | [ "load for the last 1 min" "load for the last 5 min" "load for the last 15 min"] + +| delay | process.event_loop_delay | ms +|=== + +The log interval is the same as the interval at which system and process information is refreshed and is configurable under `ops.interval`: + +[source,yaml] +---- +ops.interval: 5000 +---- + +The minimum interval is 100ms and defaults to 5000ms. + +[[request-response-logger]] +**Request and Response Logs** + +The `http.server.response` logger is configured with `debug` level and will automatically output +data about http requests and responses occurring on the {kib} server. +The message contains some high-level information, and the corresponding log meta contains the following: + +[options="header"] +|=== + +| Meta property | Description | Format + +| client.ip | IP address of the requesting client | ip + +| http.request.method | http verb for the request (uppercase) | string + +| http.request.mime_type | (optional) mime as specified in the headers | string + +| http.request.referrer | (optional) referrer | string + +| http.request.headers | request headers | object + +| http.response.body.bytes | (optional) Calculated response payload size in bytes | number + +| http.response.status_code | status code returned | number + +| http.response.headers | response headers | object + +| http.response.responseTime | (optional) Calculated response time in ms | number + +| url.path | request path | string + +| url.query | (optional) request query string | string + +| user_agent.original | raw user-agent string provided in request headers | string + +|=== + +=== Usage + +Usage is very straightforward, one should just get a logger for a specific context and use it to log messages with +different log level. + +[source,typescript] +---- +const logger = kibana.logger.get('server'); + +logger.trace('Message with `trace` log level.'); +logger.debug('Message with `debug` log level.'); +logger.info('Message with `info` log level.'); +logger.warn('Message with `warn` log level.'); +logger.error('Message with `error` log level.'); +logger.fatal('Message with `fatal` log level.'); + +const loggerWithNestedContext = kibana.logger.get('server', 'http'); +loggerWithNestedContext.trace('Message with `trace` log level.'); +loggerWithNestedContext.debug('Message with `debug` log level.'); +---- + +And assuming logger for `server` name with `console` appender and `trace` level was used, console output will look like this: +[source,bash] +---- +[2017-07-25T11:54:41.639-07:00][TRACE][server] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server] Message with `debug` log level. +[2017-07-25T11:54:41.639-07:00][INFO ][server] Message with `info` log level. +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. + +[2017-07-25T11:54:41.639-07:00][TRACE][server.http] Message with `trace` log level. +[2017-07-25T11:54:41.639-07:00][DEBUG][server.http] Message with `debug` log level. +---- + +The log will be less verbose with `warn` level for the `server` logger: +[source,bash] +---- +[2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. +[2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. +[2017-07-25T11:54:41.639-07:00][FATAL][server] Message with `fatal` log level. +---- diff --git a/docs/developer/architecture/core/patterns-scoped-services.asciidoc b/docs/developer/architecture/core/patterns-scoped-services.asciidoc new file mode 100644 index 0000000000000..d4618684fc7e4 --- /dev/null +++ b/docs/developer/architecture/core/patterns-scoped-services.asciidoc @@ -0,0 +1,61 @@ +[[patterns]] +== Patterns +[[scoped-services]] +=== Scoped services +Whenever Kibana needs to get access to data saved in Elasticsearch, it +should perform a check whether an end-user has access to the data. +The Kibana Platform introduced a handler interface on the server-side to perform that association +internally. Core services, that require impersonation with an incoming +request, are exposed via `context` argument of +{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandler.md[the +request handler interface.] +as + +[source,js] +---- +async function handler(context, req, res) { + const data = await context.core.elasticsearch.client.asCurrentUser('ping'); +} +---- + +The +{kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md[request +handler context] exposes the following scoped *core* services: + +* {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md[`context.savedObjects.client`] +* {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iscopedclusterclient.md[`context.elasticsearch.client`] +* {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.iuisettingsclient.md[`context.uiSettings.client`] + +==== Declare a custom scoped service + +Plugins can extend the handler context with a custom API that will be +available to the plugin itself and all dependent plugins. For example, +the plugin creates a custom Elasticsearch client and wants to use it via +the request handler context: + +[source,typescript] +---- +import type { CoreSetup, RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; + +interface MyRequestHandlerContext extends RequestHandlerContext { + myPlugin: { + client: IScopedClusterClient; + }; +} + +class MyPlugin { + setup(core: CoreSetup) { + const client = core.elasticsearch.createClient('myClient'); + core.http.registerRouteHandlerContext('myPlugin', (context, req, res) => { + return { client: client.asScoped(req) }; + }); + const router = core.http.createRouter(); + router.get( + { path: '/api/my-plugin/', validate: … }, + async (context, req, res) => { + // context type is inferred as MyPluginContext + const data = await context.myPlugin.client.asCurrentUser('endpoint'); + } + ); + } +---- diff --git a/docs/developer/architecture/core/saved-objects-service.asciidoc b/docs/developer/architecture/core/saved-objects-service.asciidoc index 047c3dffa6358..fa7fc4233259d 100644 --- a/docs/developer/architecture/core/saved-objects-service.asciidoc +++ b/docs/developer/architecture/core/saved-objects-service.asciidoc @@ -1,6 +1,8 @@ [[saved-objects-service]] == Saved Objects service +NOTE: The Saved Objects service is available both server and client side. + `Saved Objects service` allows {kib} plugins to use {es} like a primary database. Think of it as an Object Document Mapper for {es}. Once a plugin has registered one or more Saved Object types, the Saved Objects client @@ -28,7 +30,9 @@ spaces. This document contains developer guidelines and best-practices for plugins wanting to use Saved Objects. -=== Registering a Saved Object type +=== Server side usage + +==== Registering a Saved Object type Saved object type definitions should be defined in their own `my_plugin/server/saved_objects` directory. The folder should contain a file per type, named after the snake_case name of the type, and an `index.ts` file exporting all the types. @@ -83,7 +87,7 @@ export class MyPlugin implements Plugin { } ---- -=== Mappings +==== Mappings Each Saved Object type can define it's own {es} field mappings. Because multiple Saved Object types can share the same index, mappings defined by a type will be nested under a top-level field that matches the type name. @@ -149,59 +153,6 @@ should carefully consider the fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the `.kibana` index. -=== References -When a Saved Object declares `references` to other Saved Objects, the -Saved Objects Export API will automatically export the target object with all -of it's references. This makes it easy for users to export the entire -reference graph of an object. - -If a Saved Object can't be used on it's own, that is, it needs other objects -to exist for a feature to function correctly, that Saved Object should declare -references to all the objects it requires. For example, a `dashboard` -object might have panels for several `visualization` objects. When these -`visualization` objects don't exist, the dashboard cannot be rendered -correctly. The `dashboard` object should declare references to all it's -visualizations. - -However, `visualization` objects can continue to be rendered or embedded into -other dashboards even if the `dashboard` it was originally embedded into -doesn't exist. As a result, `visualization` objects should not declare -references to `dashboard` objects. - -For each referenced object, an `id`, `type` and `name` are added to the -`references` array: - -[source, typescript] ----- -router.get( - { path: '/some-path', validate: false }, - async (context, req, res) => { - const object = await context.core.savedObjects.client.create( - 'dashboard', - { - title: 'my dashboard', - panels: [ - { visualization: 'vis1' }, // <1> - ], - indexPattern: 'indexPattern1' - }, - { references: [ - { id: '...', type: 'visualization', name: 'vis1' }, - { id: '...', type: 'index_pattern', name: 'indexPattern1' }, - ] - } - ) - ... - } -); ----- -<1> Note how `dashboard.panels[0].visualization` stores the `name` property of -the reference (not the `id` directly) to be able to uniquely identify this -reference. This guarantees that the id the reference points to always remains -up to date. If a visualization `id` was directly stored in -`dashboard.panels[0].visualization` there is a risk that this `id` gets -updated without updating the reference in the references array. - ==== Writing Migrations Saved Objects support schema changes between Kibana versions, which we call @@ -308,4 +259,60 @@ point in time. It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a bug -in this code, there's really no reason not to aim for 100% test code coverage. \ No newline at end of file +in this code, there's really no reason not to aim for 100% test code coverage. + +=== Client side usage + +==== References + +When a Saved Object declares `references` to other Saved Objects, the +Saved Objects Export API will automatically export the target object with all +of its references. This makes it easy for users to export the entire +reference graph of an object. + +If a Saved Object can't be used on its own, that is, it needs other objects +to exist for a feature to function correctly, that Saved Object should declare +references to all the objects it requires. For example, a `dashboard` +object might have panels for several `visualization` objects. When these +`visualization` objects don't exist, the dashboard cannot be rendered +correctly. The `dashboard` object should declare references to all its +visualizations. + +However, `visualization` objects can continue to be rendered or embedded into +other dashboards even if the `dashboard` it was originally embedded into +doesn't exist. As a result, `visualization` objects should not declare +references to `dashboard` objects. + +For each referenced object, an `id`, `type` and `name` are added to the +`references` array: + +[source, typescript] +---- +router.get( + { path: '/some-path', validate: false }, + async (context, req, res) => { + const object = await context.core.savedObjects.client.create( + 'dashboard', + { + title: 'my dashboard', + panels: [ + { visualization: 'vis1' }, // <1> + ], + indexPattern: 'indexPattern1' + }, + { references: [ + { id: '...', type: 'visualization', name: 'vis1' }, + { id: '...', type: 'index_pattern', name: 'indexPattern1' }, + ] + } + ) + ... + } +); +---- +<1> Note how `dashboard.panels[0].visualization` stores the `name` property of +the reference (not the `id` directly) to be able to uniquely identify this +reference. This guarantees that the id the reference points to always remains +up to date. If a visualization `id` was directly stored in +`dashboard.panels[0].visualization` there is a risk that this `id` gets +updated without updating the reference in the references array. diff --git a/docs/developer/architecture/core/uisettings-service.asciidoc b/docs/developer/architecture/core/uisettings-service.asciidoc new file mode 100644 index 0000000000000..85ed9c9eabc72 --- /dev/null +++ b/docs/developer/architecture/core/uisettings-service.asciidoc @@ -0,0 +1,40 @@ +[[ui-settings-service]] +== UI settings service + +NOTE: The UI settings service is available both server and client side. + +=== Server side usage + +The program interface to <>. +It makes it possible for Kibana plugins to extend Kibana UI Settings Management with custom settings. + +See: + +- {kib-repo}blob/{branch}/docs/development/core/server/kibana-plugin-core-server.uisettingsservicesetup.register.md[UI settings service Setup API docs] + +[source,typescript] +---- +import { schema } from '@kbn/config-schema'; +import type { CoreSetup,Plugin } from 'kibana/server'; + +export class MyPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register({ + custom: { + value: '42', + schema: schema.string(), + }, + }); + const router = core.http.createRouter(); + router.get({ + path: 'my_plugin/{id}', + validate: …, + }, + async (context, request, response) => { + const customSetting = await context.uiSettings.client.get('custom'); + … + }); + } +} + +---- diff --git a/docs/developer/architecture/index.asciidoc b/docs/developer/architecture/index.asciidoc index 4bdd693979b49..1a0e7bab2f8f8 100644 --- a/docs/developer/architecture/index.asciidoc +++ b/docs/developer/architecture/index.asciidoc @@ -29,6 +29,24 @@ include::kibana-platform-plugin-api.asciidoc[leveloffset=+1] include::core/index.asciidoc[leveloffset=+1] +include::core/application_service.asciidoc[leveloffset=+1] + +include::core/configuration-service.asciidoc[leveloffset=+1] + +include::core/elasticsearch-service.asciidoc[leveloffset=+1] + +include::core/http-service.asciidoc[leveloffset=+1] + +include::core/logging-service.asciidoc[leveloffset=+1] + +include::core/logging-configuration-migration.asciidoc[leveloffset=+1] + +include::core/saved-objects-service.asciidoc[leveloffset=+1] + +include::core/uisettings-service.asciidoc[leveloffset=+1] + +include::core/patterns-scoped-services.asciidoc[leveloffset=+1] + include::security/index.asciidoc[leveloffset=+1] include::add-data-tutorials.asciidoc[leveloffset=+1] diff --git a/docs/developer/contributing/index.asciidoc b/docs/developer/contributing/index.asciidoc index bbf2903491bf6..1cf96d19bfb2b 100644 --- a/docs/developer/contributing/index.asciidoc +++ b/docs/developer/contributing/index.asciidoc @@ -1,7 +1,7 @@ [[contributing]] == Contributing -Whether you want to fix a bug, implement a feature, or add some other improvements or apis, the following sections will +Whether you want to fix a bug, implement a feature, add an improvement, or add APIs, the following sections will guide you on the process. After committing your code, check out the link:https://www.elastic.co/community/contributor[Elastic Contributor Program] where you can earn points and rewards for your contributions. Read <> to get your environment up and running, then read <>. @@ -53,24 +53,27 @@ To use a single paragraph of text, enter a `Release note:` or `## Release note` When you create the Release Notes text, use the following best practices: -* Use present tense. +* Use active voice. * Use sentence case. -* When you create a feature PR, start with `Adds`. -* When you create an enhancement PR, start with `Improves`. -* When you create a bug fix PR, start with `Fixes`. -* When you create a deprecation PR, start with `Deprecates`. +* When you create a PR that adds a feature, start with `Adds`. +* When you create a PR that improves an existing feature, start with `Improves`. +* When you create a PR that fixes existing functionality, start with `Fixes`. +* When you create a PR that deprecates functionality, start with `Deprecates`. [discrete] ==== Add your labels +To make sure that your PR is included in the Release Notes, add the right label. + [arabic] . Label the PR with the targeted version (ex: `v7.3.0`). . Label the PR with the appropriate GitHub labels: - * For a new feature or functionality, use `release_note:enhancement`. - * For an external-facing fix, use `release_note:fix`. We do not include docs, build, and test fixes in the Release Notes, or unreleased issues that are only on `master`. - * For a deprecated feature, use `release_note:deprecation`. - * For a breaking change, use `release_note:breaking`. - * To **NOT** include your changes in the Release Notes, use `release_note:skip`. + * `release_note:feature` — New user-facing features, significant enhancements to features, and significant bug fixes (in rare cases). + * `release_note:enhancement` — Minor UI changes and enhancements. + * `release_note:fix` — Fixes for bugs that existed in the previous release. + * `release_note:deprecation` — Deprecates functionality that existed in previous releases. + * `release_note:breaking` — Breaking changes that weren't present in previous releases. + * `release_note:skip` — Changes that should not appear in the Release Notes. For example, docs, build, and test fixes, or unreleased issues that are only in `master`. include::development-github.asciidoc[leveloffset=+1] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e1c2c40a31384..691d7fb82f3bc 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -452,10 +452,6 @@ using the CURL scripts in the scripts folder. |Visualize geo data from Elasticsearch or 3rd party geo-services. -|{kib-repo}blob/{branch}/x-pack/plugins/maps_legacy_licensing/README.md[mapsLegacyLicensing] -|This plugin provides access to the detailed tile map services from Elastic. - - |{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] |This plugin provides access to the machine learning features provided by Elastic. @@ -537,6 +533,10 @@ Documentation: https://www.elastic.co/guide/en/kibana/master/task-manager-produc |Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. +|{kib-repo}blob/{branch}/x-pack/plugins/timelines/README.md[timelines] +|Timelines is a plugin that provides a grid component with accompanying server side apis to help users identify events of interest and perform root cause analysis within Kibana. + + |{kib-repo}blob/{branch}/x-pack/plugins/transform/readme.md[transform] |This plugin provides access to the transforms features provided by Elastic. diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.deprecations.md b/docs/development/core/public/kibana-plugin-core-public.corestart.deprecations.md new file mode 100644 index 0000000000000..624c4868d54a7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) + +## CoreStart.deprecations property + +[DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) + +Signature: + +```typescript +deprecations: DeprecationsServiceStart; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index a7b45b318d2c9..6ad9adca53ef5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -18,6 +18,7 @@ export interface CoreStart | --- | --- | --- | | [application](./kibana-plugin-core-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | +| [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | | [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md new file mode 100644 index 0000000000000..8175da8a1893a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [getAllDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md) + +## DeprecationsServiceStart.getAllDeprecations property + +Grabs deprecations details for all domains. + +Signature: + +```typescript +getAllDeprecations: () => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md new file mode 100644 index 0000000000000..6e3472b7c3fe3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [getDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md) + +## DeprecationsServiceStart.getDeprecations property + +Grabs deprecations for a specific domain. + +Signature: + +```typescript +getDeprecations: (domainId: string) => Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md new file mode 100644 index 0000000000000..842761f6b7cea --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [isDeprecationResolvable](./kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md) + +## DeprecationsServiceStart.isDeprecationResolvable property + +Returns a boolean if the provided deprecation can be automatically resolvable. + +Signature: + +```typescript +isDeprecationResolvable: (details: DomainDeprecationDetails) => boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md new file mode 100644 index 0000000000000..0d2c963ec5547 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) + +## DeprecationsServiceStart interface + +DeprecationsService provides methods to fetch domain deprecation details from the Kibana server. + +Signature: + +```typescript +export interface DeprecationsServiceStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getAllDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getalldeprecations.md) | () => Promise<DomainDeprecationDetails[]> | Grabs deprecations details for all domains. | +| [getDeprecations](./kibana-plugin-core-public.deprecationsservicestart.getdeprecations.md) | (domainId: string) => Promise<DomainDeprecationDetails[]> | Grabs deprecations for a specific domain. | +| [isDeprecationResolvable](./kibana-plugin-core-public.deprecationsservicestart.isdeprecationresolvable.md) | (details: DomainDeprecationDetails) => boolean | Returns a boolean if the provided deprecation can be automatically resolvable. | +| [resolveDeprecation](./kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md) | (details: DomainDeprecationDetails) => Promise<ResolveDeprecationResponse> | Calls the correctiveActions.api to automatically resolve the depprecation. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md new file mode 100644 index 0000000000000..fae623fed3cc2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) > [resolveDeprecation](./kibana-plugin-core-public.deprecationsservicestart.resolvedeprecation.md) + +## DeprecationsServiceStart.resolveDeprecation property + +Calls the correctiveActions.api to automatically resolve the depprecation. + +Signature: + +```typescript +resolveDeprecation: (details: DomainDeprecationDetails) => Promise; +``` 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 6ca7a83ac0a03..860f7c3c74892 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 @@ -144,6 +144,7 @@ readonly links: { putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; + simulatePipeline: string; updateTransform: string; }>; readonly observability: Record; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 3847ab0c6183a..a9cb6729b214e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putWatch: string;
simulatePipeline: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index e9d08dcd3bf4c..32f17d5488f66 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -59,6 +59,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeUserBanner](./kibana-plugin-core-public.chromeuserbanner.md) | | | [CoreSetup](./kibana-plugin-core-public.coresetup.md) | Core services exposed to the Plugin setup lifecycle | | [CoreStart](./kibana-plugin-core-public.corestart.md) | Core services exposed to the Plugin start lifecycle | +| [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | DeprecationsService provides methods to fetch domain deprecation details from the Kibana server. | | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. | | [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | @@ -164,6 +165,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) | Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) | | [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | +| [ResolveDeprecationResponse](./kibana-plugin-core-public.resolvedeprecationresponse.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedeprecationresponse.md b/docs/development/core/public/kibana-plugin-core-public.resolvedeprecationresponse.md new file mode 100644 index 0000000000000..928bf8c07004e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedeprecationresponse.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolveDeprecationResponse](./kibana-plugin-core-public.resolvedeprecationresponse.md) + +## ResolveDeprecationResponse type + +Signature: + +```typescript +export declare type ResolveDeprecationResponse = { + status: 'ok'; +} | { + status: 'fail'; + reason: string; +}; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.deprecations.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.deprecations.md new file mode 100644 index 0000000000000..436cc29b6e343 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [deprecations](./kibana-plugin-core-server.coresetup.deprecations.md) + +## CoreSetup.deprecations property + +[DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) + +Signature: + +```typescript +deprecations: DeprecationsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 1171dbad570ce..b37ac80db87d6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -18,6 +18,7 @@ export interface CoreSetupCapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | | [context](./kibana-plugin-core-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | +| [deprecations](./kibana-plugin-core-server.coresetup.deprecations.md) | DeprecationsServiceSetup | [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md new file mode 100644 index 0000000000000..e362bc4e0329c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) + +## DeprecationsDetails.correctiveActions property + +Signature: + +```typescript +correctiveActions: { + api?: { + path: string; + method: 'POST' | 'PUT'; + body?: { + [key: string]: any; + }; + }; + manualSteps?: string[]; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md new file mode 100644 index 0000000000000..467d6d76cf842 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) + +## DeprecationsDetails.documentationUrl property + +Signature: + +```typescript +documentationUrl?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md new file mode 100644 index 0000000000000..64ad22e2c87fb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [level](./kibana-plugin-core-server.deprecationsdetails.level.md) + +## DeprecationsDetails.level property + +levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. + +Signature: + +```typescript +level: 'warning' | 'critical' | 'fetch_error'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md new file mode 100644 index 0000000000000..bb77e4247711f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) + +## DeprecationsDetails interface + +Signature: + +```typescript +export interface DeprecationsDetails +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps?: string[];
} | | +| [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | | +| [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | +| [message](./kibana-plugin-core-server.deprecationsdetails.message.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md new file mode 100644 index 0000000000000..d79a4c9bd7995 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [message](./kibana-plugin-core-server.deprecationsdetails.message.md) + +## DeprecationsDetails.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md new file mode 100644 index 0000000000000..7d9d3dcdda4da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -0,0 +1,95 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) + +## DeprecationsServiceSetup interface + +The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version. + +The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience. + +If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. + +Signature: + +```typescript +export interface DeprecationsServiceSetup +``` + +## Example + + +```ts +import { DeprecationsDetails, GetDeprecationsContext, CoreSetup } from 'src/core/server'; + +async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { + const deprecations: DeprecationsDetails[] = []; + const count = await getTimelionSheetsCount(savedObjectsClient); + + if (count > 0) { + // Example of a manual correctiveAction + deprecations.push({ + message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + documentationUrl: + 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', + level: 'warning', + correctiveActions: { + manualSteps: [ + 'Navigate to the Kibana Dashboard and click "Create dashboard".', + 'Select Timelion from the "New Visualization" window.', + 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', + 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', + 'In the toolbar, click Save.', + 'On the Save visualization window, enter the visualization Title, then click Save and return.', + ], + }, + }); + } + + // Example of an api correctiveAction + deprecations.push({ + "message": "User 'test_dashboard_user' is using a deprecated role: 'kibana_user'", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", + "level": "critical", + "correctiveActions": { + "api": { + "path": "/internal/security/users/test_dashboard_user", + "method": "POST", + "body": { + "username": "test_dashboard_user", + "roles": [ + "machine_learning_user", + "enrich_user", + "kibana_admin" + ], + "full_name": "Alison Goryachev", + "email": "alisongoryachev@gmail.com", + "metadata": {}, + "enabled": true + } + }, + "manualSteps": [ + "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", + "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role." + ] + }, + }); + + return deprecations; +} + + +export class Plugin() { + setup: (core: CoreSetup) => { + core.deprecations.registerDeprecations({ getDeprecations }); + } +} + +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [registerDeprecations](./kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md) | (deprecationContext: RegisterDeprecationsConfig) => void | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md new file mode 100644 index 0000000000000..07c2a3ad0ce55 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) > [registerDeprecations](./kibana-plugin-core-server.deprecationsservicesetup.registerdeprecations.md) + +## DeprecationsServiceSetup.registerDeprecations property + +Signature: + +```typescript +registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.esclient.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.esclient.md new file mode 100644 index 0000000000000..70c1864bf905f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.esclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) > [esClient](./kibana-plugin-core-server.getdeprecationscontext.esclient.md) + +## GetDeprecationsContext.esClient property + +Signature: + +```typescript +esClient: IScopedClusterClient; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md new file mode 100644 index 0000000000000..1018444f0849a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) + +## GetDeprecationsContext interface + +Signature: + +```typescript +export interface GetDeprecationsContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [esClient](./kibana-plugin-core-server.getdeprecationscontext.esclient.md) | IScopedClusterClient | | +| [savedObjectsClient](./kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md) | SavedObjectsClientContract | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md new file mode 100644 index 0000000000000..66da52d3b5824 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) > [savedObjectsClient](./kibana-plugin-core-server.getdeprecationscontext.savedobjectsclient.md) + +## GetDeprecationsContext.savedObjectsClient property + +Signature: + +```typescript +savedObjectsClient: SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index 395c26a6e4bf6..8ddc0da5f1b28 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -10,10 +10,10 @@ Set of helpers used to create `KibanaResponse` to form HTTP response on an incom ```typescript kibanaResponseFactory: { - custom: | Error | Buffer | { + custom: | Error | Buffer | Stream | { message: string | Error; attributes?: Record | undefined; - } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md deleted file mode 100644 index 67f2cf0cdcc7c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) - -## LegacyServiceSetupDeps.core property - -Signature: - -```typescript -core: LegacyCoreSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md deleted file mode 100644 index a5c1d59be06d3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) - -## LegacyServiceSetupDeps interface - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface LegacyServiceSetupDeps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup | | -| [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown> | | -| [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) | UiPlugins | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md deleted file mode 100644 index 032762904640b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [plugins](./kibana-plugin-core-server.legacyservicesetupdeps.plugins.md) - -## LegacyServiceSetupDeps.plugins property - -Signature: - -```typescript -plugins: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md deleted file mode 100644 index d19a7dfcbfcfa..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) > [uiPlugins](./kibana-plugin-core-server.legacyservicesetupdeps.uiplugins.md) - -## LegacyServiceSetupDeps.uiPlugins property - -Signature: - -```typescript -uiPlugins: UiPlugins; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md deleted file mode 100644 index 17369e00a7068..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.core.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) > [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) - -## LegacyServiceStartDeps.core property - -Signature: - -```typescript -core: LegacyCoreStart; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md deleted file mode 100644 index d6f6b38b79f84..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) - -## LegacyServiceStartDeps interface - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -export interface LegacyServiceStartDeps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [core](./kibana-plugin-core-server.legacyservicestartdeps.core.md) | LegacyCoreStart | | -| [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) | Record<string, unknown> | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md b/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md deleted file mode 100644 index 4634bf21fb42c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.legacyservicestartdeps.plugins.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) > [plugins](./kibana-plugin-core-server.legacyservicestartdeps.plugins.md) - -## LegacyServiceStartDeps.plugins property - -Signature: - -```typescript -plugins: Record; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 4bf00d2da6e23..3bbdf8c703ab1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -69,13 +69,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DeprecationAPIClientParams](./kibana-plugin-core-server.deprecationapiclientparams.md) | | | [DeprecationAPIResponse](./kibana-plugin-core-server.deprecationapiresponse.md) | | | [DeprecationInfo](./kibana-plugin-core-server.deprecationinfo.md) | | +| [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) | | | [DeprecationSettings](./kibana-plugin-core-server.deprecationsettings.md) | UiSettings deprecation field options. | +| [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version.The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience.If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. | | [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | | [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) | | | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) | | | [GetResponse](./kibana-plugin-core-server.getresponse.md) | | | [HttpAuth](./kibana-plugin-core-server.httpauth.md) | | | [HttpResources](./kibana-plugin-core-server.httpresources.md) | HttpResources service is responsible for serving static & dynamic assets for Kibana application via HTTP. Provides API allowing plug-ins to respond with: - a pre-configured HTML page bootstrapping Kibana client app - custom HTML page - custom JS script file. | @@ -107,8 +110,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. | | [LegacyRequest](./kibana-plugin-core-server.legacyrequest.md) | | -| [LegacyServiceSetupDeps](./kibana-plugin-core-server.legacyservicesetupdeps.md) | | -| [LegacyServiceStartDeps](./kibana-plugin-core-server.legacyservicestartdeps.md) | | | [LoggerContextConfigInput](./kibana-plugin-core-server.loggercontextconfiginput.md) | | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | @@ -128,6 +129,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | +| [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) | | | [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) | Defines a set of additional options for the resolveCapabilities method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md). | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | diff --git a/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md new file mode 100644 index 0000000000000..cf008725ff15b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) > [getDeprecations](./kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md) + +## RegisterDeprecationsConfig.getDeprecations property + +Signature: + +```typescript +getDeprecations: (context: GetDeprecationsContext) => MaybePromise; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md new file mode 100644 index 0000000000000..59e6d406f84bf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.registerdeprecationsconfig.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RegisterDeprecationsConfig](./kibana-plugin-core-server.registerdeprecationsconfig.md) + +## RegisterDeprecationsConfig interface + +Signature: + +```typescript +export interface RegisterDeprecationsConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getDeprecations](./kibana-plugin-core-server.registerdeprecationsconfig.getdeprecations.md) | (context: GetDeprecationsContext) => MaybePromise<DeprecationsDetails[]> | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md index a1fde4dec25b1..6e7b753320270 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md @@ -7,9 +7,9 @@ Signature: ```typescript -fieldIsTimeField(): boolean | "" | undefined; +fieldIsTimeField(): boolean; ``` Returns: -`boolean | "" | undefined` +`boolean` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md index c0ba1bbeea334..22f8994747aa2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -23,6 +23,7 @@ export declare class AggConfigs | [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) | | IAggConfig[] | | | [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) | | <T extends AggConfig = AggConfig>(params: CreateAggConfigParams, { addToAggConfigs }?: {
addToAggConfigs?: boolean | undefined;
}) => T | | | [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) | | IndexPattern | | +| [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) | | string[] | | | [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) | | TimeRange | | ## Methods @@ -43,6 +44,7 @@ export declare class AggConfigs | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} | | [jsonDataEquals(aggConfigs)](./kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md) | | Data-by-data comparison of this Aggregation Ignores the non-array indexes | | [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) | | | +| [setTimeFields(timeFields)](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) | | | | [setTimeRange(timeRange)](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) | | | | [toDsl(hierarchical)](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimefields.md new file mode 100644 index 0000000000000..31eadc5756d3d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimefields.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [setTimeFields](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) + +## AggConfigs.setTimeFields() method + +Signature: + +```typescript +setTimeFields(timeFields: string[] | undefined): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| timeFields | string[] | undefined | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timefields.md new file mode 100644 index 0000000000000..903370fd8eb84 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timefields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) + +## AggConfigs.timeFields property + +Signature: + +```typescript +timeFields?: string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md new file mode 100644 index 0000000000000..4e432b8d365a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggFunctionsMapping](./kibana-plugin-plugins-data-public.aggfunctionsmapping.md) > [aggSinglePercentile](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md) + +## AggFunctionsMapping.aggSinglePercentile property + +Signature: + +```typescript +aggSinglePercentile: ReturnType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md index 05388e2b86d7b..852c6d5f1c00b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md @@ -45,6 +45,7 @@ export interface AggFunctionsMapping | [aggRange](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggrange.md) | ReturnType<typeof aggRange> | | | [aggSerialDiff](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggserialdiff.md) | ReturnType<typeof aggSerialDiff> | | | [aggSignificantTerms](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsignificantterms.md) | ReturnType<typeof aggSignificantTerms> | | +| [aggSinglePercentile](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md) | ReturnType<typeof aggSinglePercentile> | | | [aggStdDeviation](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggstddeviation.md) | ReturnType<typeof aggStdDeviation> | | | [aggSum](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsum.md) | ReturnType<typeof aggSum> | | | [aggTerms](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggterms.md) | ReturnType<typeof aggTerms> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md index 3b5cecf1a0b82..bdae3ec738ac3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md @@ -32,6 +32,7 @@ export declare enum METRIC_TYPES | PERCENTILE\_RANKS | "percentile_ranks" | | | PERCENTILES | "percentiles" | | | SERIAL\_DIFF | "serial_diff" | | +| SINGLE\_PERCENTILE | "single_percentile" | | | STD\_DEV | "std_dev" | | | SUM | "sum" | | | SUM\_BUCKET | "sum_bucket" | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 440fd25993d64..cfaad01c029ea 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -14,7 +14,7 @@ search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; @@ -47,6 +47,11 @@ search: { intervalLabel: string; })[]; getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; + getDateHistogramMetaDataByDatatableColumn: (column: import("../../expressions").DatatableColumn) => { + interval: string | undefined; + timeZone: string | undefined; + timeRange: import("../common").TimeRange | undefined; + } | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md new file mode 100644 index 0000000000000..d1418d7245d73 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggFunctionsMapping](./kibana-plugin-plugins-data-server.aggfunctionsmapping.md) > [aggSinglePercentile](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md) + +## AggFunctionsMapping.aggSinglePercentile property + +Signature: + +```typescript +aggSinglePercentile: ReturnType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md index 86bf797572b09..6b5f854c155f3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md @@ -45,6 +45,7 @@ export interface AggFunctionsMapping | [aggRange](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggrange.md) | ReturnType<typeof aggRange> | | | [aggSerialDiff](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggserialdiff.md) | ReturnType<typeof aggSerialDiff> | | | [aggSignificantTerms](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsignificantterms.md) | ReturnType<typeof aggSignificantTerms> | | +| [aggSinglePercentile](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md) | ReturnType<typeof aggSinglePercentile> | | | [aggStdDeviation](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggstddeviation.md) | ReturnType<typeof aggStdDeviation> | | | [aggSum](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsum.md) | ReturnType<typeof aggSum> | | | [aggTerms](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggterms.md) | ReturnType<typeof aggTerms> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md index d408f00e33c9e..b5c7d8931ad4b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -14,6 +14,6 @@ export declare class IndexPatternsServiceProvider implements PluginSignature: ```typescript -setup(core: CoreSetup, { expressions }: IndexPatternsServiceSetupDeps): void; +setup(core: CoreSetup, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup<DataPluginStartDependencies, DataPluginStart> | | -| { expressions } | IndexPatternsServiceSetupDeps | | +| core | CoreSetup<IndexPatternsServiceStartDeps, DataPluginStart> | | +| { expressions, usageCollection } | IndexPatternsServiceSetupDeps | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 98f9310c6d98c..88079bb2fa3cb 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md index 250173d11a056..37f53af8971b3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md @@ -32,6 +32,7 @@ export declare enum METRIC_TYPES | PERCENTILE\_RANKS | "percentile_ranks" | | | PERCENTILES | "percentiles" | | | SERIAL\_DIFF | "serial_diff" | | +| SINGLE\_PERCENTILE | "single_percentile" | | | STD\_DEV | "std_dev" | | | SUM | "sum" | | | SUM\_BUCKET | "sum_bucket" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 025cab9f48c1a..f4404521561d2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md index 4f8a0beefa421..0911c3e86964d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md @@ -14,7 +14,7 @@ search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 9971a6f574f9c..0306be3eb670d 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -110,7 +110,7 @@ image::discover/images/read-only-badge.png[Example of Discover's read only acces ==== Save a search To save the current search: -. Click *Save* in the Kibana toolbar. +. Click *Save* in the toolbar. . Enter a name for the search and click *Save*. To import, export, and delete saved searches, open the main menu, @@ -119,7 +119,7 @@ then click *Stack Management > Saved Objects*. ==== Open a saved search To load a saved search into Discover: -. Click *Open* in the Kibana toolbar. +. Click *Open* in the toolbar. . Select the search you want to open. If the saved search is associated with a different index pattern than is currently diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 446b6a2cfd851..a9de1888465f7 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -209,6 +209,32 @@ from *{stack-monitor-app}*. Turns off all unnecessary animations in the {kib} UI. Refresh the page to apply the changes. +[float] +[[kibana-banners-settings]] +==== Banners + +[NOTE] +==== +Banners are a https://www.elastic.co/subscriptions[subscription feature]. +==== + +[horizontal] +[[banners-placement]]`banners:placement`:: +Set to `Top` to display a banner above the Elastic header for this space. Defaults to the value of +the `xpack.banners.placement` configuration property. + +[[banners-textcontent]]`banners:textContent`:: +The text to display inside the banner for this space, either plain text or Markdown. +Defaults to the value of the `xpack.banners.textContent` configuration property. + +[[banners-textcolor]]`banners:textColor`:: +The color for the banner text for this space. Defaults to the value of +the `xpack.banners.textColor` configuration property. + +[[banners-backgroundcolor]]`banners:backgroundColor`:: +The color of the banner background for this space. Defaults to the value of +the `xpack.banners.backgroundColor` configuration property. + [float] [[kibana-dashboard-settings]] ==== Dashboard diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 5cd5c1ffd6248..505f6853c7906 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -78,6 +78,7 @@ include::field-formatters/color-formatter.asciidoc[] [[scripted-fields]] === Scripted fields +deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default @@ -87,7 +88,7 @@ WARNING: Computing data on the fly with scripted fields can be very resource int {kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the +When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the {ref}/modules-scripting-painless.html[Painless] scripting language. You can reference any single value numeric field in your expressions, for example: diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index fb4250368086e..0218bac58815a 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,30 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +[discrete] +[[import-geospatial-privileges]] +=== Security privileges + +The {stack-security-features} provide roles and privileges that control which users can upload files. +You can manage your roles, privileges, and +spaces in **{stack-manage-app}** in {kib}. For more information, see +{ref}/security-privileges.html[Security privileges], +<>, and <>. + +To upload GeoJSON files in {kib} with *Maps*, you must have: + +* The `all` {kib} privilege for *Maps*. +* The `all` {kib} privilege for *Index Pattern Management*. +* The `create` and `create_index` index privileges for destination indices. +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. + +To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: + +* The `manage_pipeline` cluster privilege. +* The `read` {kib} privilege for *Machine Learning*. +* The `machine_learning_admin` or `machine_learning_user` role. + + [discrete] === Upload CSV with latitude and longitude columns diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 265bf6bfaea30..7f4af952653e7 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -76,9 +76,8 @@ then accumulates the most relevant documents based on sort order for each entry To enable top hits: -. Click *Add layer*, then select the *Documents* layer. +. Click *Add layer*, then select the *Top hits per entity* layer. . Configure *Index pattern* and *Geospatial field*. -. In *Scaling*, select *Show top hits per entity*. . Set *Entity* to the field that identifies entities in your documents. This field will be used in the terms aggregation to group your documents into entity buckets. . Set *Documents per entity* to configure the maximum number of documents accumulated per entity. diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 6a2228161845e..2115c16a889c6 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -23,8 +23,6 @@ Select the appropriate *Scaling* option for your use case. * *Limit results to 10000.* The layer displays features from the first `index.max_result_window` documents. Results exceeding `index.max_result_window` are not displayed. -* *Show top hits per entity.* The layer displays the <>. - * *Show clusters when results exceed 10000.* When results exceed `index.max_result_window`, the layer uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into clusters and displays metrics for each cluster. When results are less then `index.max_result_window`, the layer displays features from individual documents. * *Use vector tiles.* Vector tiles partition your map into 6 to 8 tiles. @@ -36,6 +34,9 @@ Tiles exceeding `index.max_result_window` have a visual indicator when there are *Point to point*:: Aggregated data paths between the source and destination. The index must contain at least 2 fields mapped as {ref}/geo-point.html[geo_point], source and destination. +*Top hits per entity*:: The layer displays the <>. +The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. + *Tracks*:: Create lines from points. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 52d1d63ce0653..acb343191609d 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -50,46 +50,56 @@ for example, `logstash-*`. [float] ==== Default logging timezone is now the system's timezone -*Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. +*Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. *Impact:* To restore the previous behavior, in kibana.yml use the pattern layout, with a date modifier: [source,yaml] ------------------- logging: appenders: - console: - kind: console + custom: + type: console layout: - kind: pattern + type: pattern pattern: "%date{ISO8601_TZ}{UTC}" ------------------- See https://github.com/elastic/kibana/pull/90368 for more details. [float] ==== Responses are never logged by default -*Details:* Previously responses would be logged if either `logging.json` was true, `logging.dest` was specified, or a `TTY` was detected. +*Details:* Previously responses would be logged if either `logging.json` was true, `logging.dest` was specified, or a `TTY` was detected. With the new logging configuration, these are provided by a dedicated logger. -*Impact:* To restore the previous behavior, in kibana.yml enable `debug` logs for the `http.server.response` context under `logging.loggers`: +*Impact:* To restore the previous behavior, in `kibana.yml` enable `debug` for the `http.server.response` logger: [source,yaml] ------------------- logging: + appenders: + custom: + type: console + layout: + type: pattern loggers: - - context: http.server.response - appenders: [console] + - name: http.server.response + appenders: [custom] level: debug ------------------- See https://github.com/elastic/kibana/pull/87939 for more details. [float] ==== Logging destination is specified by the appender -*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. +*Details:* Previously log destination would be `stdout` and could be changed to `file` using `logging.dest`. With the new logging configuration, you can specify the destination using appenders. -*Impact:* To restore the previous behavior, in `kibana.yml` use the `console` appender to send logs to `stdout`. +*Impact:* To restore the previous behavior and log records to *stdout*, in `kibana.yml` use an appender with `type: console`. [source,yaml] ------------------- logging: + appenders: + custom: + type: console + layout: + type: pattern root: - appenders: [default, console] + appenders: [default, custom] ------------------- To send logs to `file` with a given file path, you should define a custom appender with `type:file`: @@ -107,16 +117,15 @@ logging: ------------------- [float] -==== Specify log event output with root -*Details:* Previously logging output would be specified by `logging.silent` (none), 'logging.quiet' (error messages only) and `logging.verbose` (all). +==== Set log verbosity with root +*Details:* Previously logging output would be specified by `logging.silent` (none), `logging.quiet` (error messages only) and `logging.verbose` (all). With the new logging configuration, set the minimum required log level. -*Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level` as one of `off`, `error`, `all`: +*Impact:* To restore the previous behavior, in `kibana.yml` specify `logging.root.level`: [source,yaml] ------------------- # suppress all logs logging: root: - appenders: [default] level: off ------------------- @@ -125,7 +134,6 @@ logging: # only log error messages logging: root: - appenders: [default] level: error ------------------- @@ -134,54 +142,14 @@ logging: # log all events logging: root: - appenders: [default] level: all ------------------- [float] -==== Suppress all log output with root -*Details:* Previously all logging output would be suppressed if `logging.silent` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'off'. -[source,yaml] -------------------- -logging: - root: - appenders: [default] - level: off -------------------- +==== Declare log message format +*Details:* Previously all events would be logged in `json` format when `logging.json` was true. With the new logging configuration you can specify the output format with layouts. You can choose between `json` and pattern format depending on your needs. -[float] -==== Suppress log output with root -*Details:* Previously all logging output other than error messages would be suppressed if `logging.quiet` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'error'. -[source,yaml] -------------------- -logging: - root: - appenders: [default] - level: error -------------------- - -[float] -==== Log all output with root -*Details:* Previously all events would be logged if `logging.verbose` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` turn `logging.root.level` to 'all'. -[source,yaml] -------------------- -logging: - root: - appenders: [default] - level: all -------------------- - -[float] -==== Declare log message format for each custom appender -*Details:* Previously all events would be logged in `json` format when `logging.json` was true. - -*Impact:* To restore the previous behavior, in `kibana.yml` configure the logging format for each custom appender with the `appender.layout` property. There is no default for custom appenders and each one must be configured expilictly. +*Impact:* To restore the previous behavior, in `kibana.yml` configure the logging format for each custom appender with the `appender.layout` property. There is no default for custom appenders and each one must be configured expilictly. [source,yaml] ------------------- @@ -352,6 +320,15 @@ All supported operating systems support using systemd service files. Any system *Impact:* Any installations using `.deb` or `.rpm` packages using SysV will need to migrate to systemd. +[float] +=== TLS v1.0 and v1.1 are disabled by default + +*Details:* +Support can be re-enabled by setting `--tls-min-1.0` in the `node.options` config file that can be found inside `kibana/config` folder or any other configured with the environment variable `KBN_PATH_CONF` (for example in Debian based system would be `/etc/kibana`). + +*Impact:* +Browser and proxy clients communicating over TLS v1.0 and v1.1. + [float] === Platform removed from root folder name for `.tar.gz` and `.zip` archives diff --git a/docs/settings/banners-settings.asciidoc b/docs/settings/banners-settings.asciidoc index 2a68cbe82f9f2..ce56d4dbe7a4d 100644 --- a/docs/settings/banners-settings.asciidoc +++ b/docs/settings/banners-settings.asciidoc @@ -9,6 +9,11 @@ Banners are disabled by default. You need to manually configure them in order to You can configure the `xpack.banners` settings in your `kibana.yml` file. +[NOTE] +==== +Banners are a https://www.elastic.co/subscriptions[subscription feature]. +==== + [[general-banners-settings-kb]] ==== General banner settings @@ -16,7 +21,7 @@ You can configure the `xpack.banners` settings in your `kibana.yml` file. |=== | `xpack.banners.placement` -| Set to `header` to enable the header banner. Defaults to `disabled`. +| Set to `top` to display a banner above the Elastic header. Defaults to `disabled`. | `xpack.banners.textContent` | The text to display inside the banner, either plain text or Markdown. @@ -27,9 +32,7 @@ You can configure the `xpack.banners` settings in your `kibana.yml` file. | `xpack.banners.backgroundColor` | The color of the banner background. Defaults to `#FFF9E8`. -|=== +| `xpack.banners.disableSpaceBanners` +| If true, per-space banner overrides will be disabled. Defaults to `false`. -[NOTE] -==== -The `banners` plugin is a https://www.elastic.co/subscriptions[subscription feature] -==== \ No newline at end of file +|=== diff --git a/docs/settings/logging-settings.asciidoc b/docs/settings/logging-settings.asciidoc new file mode 100644 index 0000000000000..aa38d54305eec --- /dev/null +++ b/docs/settings/logging-settings.asciidoc @@ -0,0 +1,173 @@ +[[logging-settings]] +=== Logging settings in {kib} +++++ +Logging settings +++++ + +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context (default) are forwarded to the legacy logging service. +The logging configuration is validated against the predefined schema and if there are +any issues with it, {kib} will fail to start with the detailed error message. + +NOTE: When you switch to the new logging configuration, you will start seeing duplicate log entries in both formats. +These will be removed when the `default` appender is no longer required. + +Here are some configuration examples for the most common logging use cases: + +[[log-to-file-example]] +==== Log to a file + +Log the default log format to a file instead of to stdout (the default). + +[source,yaml] +---- +logging: + appenders: + file: + type: file + fileName: /var/log/kibana.log + layout: + type: pattern + root: + appenders: [default, file] +---- + +[[log-in-json-ECS-example]] +==== Log in json format + +Log the default log format to json layout instead of pattern (the default). +With `json` layout log messages will be formatted as JSON strings in https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[ECS format] that includes a timestamp, log level, logger, message text and any other metadata that may be associated with the log message itself + +[source,yaml] +---- +logging: + appenders: + json-layout: + type: console + layout: + type: json + root: + appenders: [default, json-layout] +---- + +[[log-with-meta-to-stdout]] +==== Log with meta to stdout + +Include `%meta` in your pattern layout: + +[source,yaml] +---- +logging: + appenders: + console-meta: + type: console + layout: + type: pattern + pattern: "[%date] [%level] [%logger] [%meta] %message" + root: + appenders: [default, console-meta] +---- + +[[log-elasticsearch-queries]] +==== Log {es} queries + +[source,yaml] +-- +logging: + appenders: + console_appender: + type: console + layout: + type: pattern + highlight: true + root: + appenders: [default, console_appender] + level: warn + loggers: + - name: elasticsearch.query + level: debug +-- + +[[change-overall-log-level]] +==== Change overall log level. + +[source,yaml] +---- +logging: + root: + level: debug +---- + +[[customize-specific-log-records]] +==== Customize specific log records +Here is a detailed configuration example that can be used to configure _loggers_, _appenders_ and _layouts_: + +[source,yaml] +---- +logging: + appenders: + console: + type: console + layout: + type: pattern + highlight: true + file: + type: file + fileName: /var/log/kibana.log + custom: + type: console + layout: + type: pattern + pattern: "[%date][%level] %message" + json-file-appender: + type: file + fileName: /var/log/kibana-json.log + layout: + type: json + + root: + appenders: [default, console, file] + level: error + + loggers: + - name: plugins + appenders: [custom] + level: warn + - name: plugins.myPlugin + level: info + - name: server + level: fatal + - name: optimize + appenders: [console] + - name: telemetry + appenders: [json-file-appender] + level: all + - name: metrics.ops + appenders: [console] + level: debug +---- + +Here is what we get with the config above: +[options="header"] +|=== + +| Context name | Appenders | Level + +| root | console, file | error + +| plugins | custom | warn + +| plugins.myPlugin | custom | info + +| server | console, file | fatal + +| optimize | console | error + +| telemetry | json-file-appender | all + +| metrics.ops | console | debug +|=== + +NOTE: If you modify `root.appenders`, make sure to include `default`. + +// For more details about logging configuration, refer to the logging system documentation (update to include a link). diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index cef5a953fded4..9bb11f3f99a15 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -260,19 +260,21 @@ For information about {kib} memory limits, see <> setting. Defaults to `.reporting`. - | `xpack.reporting.capture.networkPolicy` | Capturing a screenshot from a {kib} page involves sending out requests for all the linked web assets. For example, a Markdown visualization can show an image from a remote server. You can configure what type of requests to allow or filter by setting a <> for Reporting. +| `xpack.reporting.index` + | deprecated:[7.11.0,This setting will be removed in 8.0.] Multitenancy by + changing `kibana.index` will not be supported starting in 8.0. See + https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes] for more + details. Reporting uses a weekly index in {es} to store the reporting job and + the report content. The index is automatically created if it does not already + exist. Configure this to a unique value, beginning with `.reporting-`, for + every {kib} instance that has a unique <> + setting. Defaults to `.reporting`. + | `xpack.reporting.roles.allow` | Specifies the roles in addition to superusers that can use reporting. Defaults to `[ "reporting_user" ]`. + diff --git a/docs/settings/search-sessions-settings.asciidoc b/docs/settings/search-sessions-settings.asciidoc index c9a9e709ac7f8..cf64d08e4806c 100644 --- a/docs/settings/search-sessions-settings.asciidoc +++ b/docs/settings/search-sessions-settings.asciidoc @@ -11,15 +11,15 @@ Configure the search session settings in your `kibana.yml` configuration file. [cols="2*<"] |=== a| `xpack.data_enhanced.` -`search.sessions:enabled` - | Set to `true` (default) to enable search sessions. +`search.sessions.enabled` +| Set to `true` (default) to enable search sessions. -a| `xpack.data.enhanced.` -`search.sessions:trackingInterval` - | The frequency for updating the state of a search session. The default is 10s. +a| `xpack.data_enhanced.` +`search.sessions.trackingInterval` +| The frequency for updating the state of a search session. The default is 10s. -a| `xpack.data.enhanced.` -`search.sessions:defaultExpiration` - | How long search session results are stored before they are deleted. - Extending a search session resets the expiration by the same value. The default is 7d. +a| `xpack.data_enhanced.` +`search.sessions.defaultExpiration` +| How long search session results are stored before they are deleted. +Extending a search session resets the expiration by the same value. The default is 7d. |=== diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 25883307e69f0..31e7b25eb66b1 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -39,11 +39,13 @@ docker pull {docker-repo}:{version} === Run Kibana on Docker for development Kibana can be quickly started and connected to a local Elasticsearch container for development or testing use with the following command: --------------------------------------------- + +[source,sh,subs="attributes"] +---- docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version} --------------------------------------------- -endif::[] +---- +endif::[] [float] [[configuring-kibana-docker]] === Configure Kibana on Docker diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 62e0f0847cbac..73b268e1e48b3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -25,12 +25,14 @@ which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* | `cpu.cgroup.path.override:` - | *deprecated* This setting has been renamed to <> -and the old name will no longer be supported as of 8.0. + | deprecated:[7.10.0,"This setting will no longer be supported as of 8.0."] + This setting has been renamed to + <>. | `cpuacct.cgroup.path.override:` - | *deprecated* This setting has been renamed to <> -and the old name will no longer be supported as of 8.0. + | deprecated:[7.10.0,"This setting will no longer be supported as of 8.0."] + This setting has been renamed to + <>. | `csp.rules:` | A https://w3c.github.io/webappsec-csp/[content-security-policy] template @@ -64,10 +66,35 @@ To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. | `elasticsearch.logQueries:` - | *deprecated* This setting is no longer used and will get removed in Kibana 8.0. Instead, set <> to `true` -This is useful for seeing the query DSL generated by applications that -currently do not have an inspector, for example Timelion and Monitoring. -*Default: `false`* + | deprecated:[7.12.0,"This setting is no longer used and will be removed in Kibana 8.0."] + Instead, configure the `elasticsearch.query` logger. + + + This is useful for seeing the query DSL generated by applications that + currently do not have an inspector, for example Timelion and Monitoring. + *Default: `false`* + +The following example shows a valid `elasticsearch.query` logger configuration: +|=== + +[source,text] +-- +logging: + appenders: + console_appender: + type: console + layout: + type: pattern + highlight: true + root: + appenders: [default, console_appender] + level: warn + loggers: + - name: elasticsearch.query + level: debug +-- + +[cols="2*<"] +|=== |[[elasticsearch-pingTimeout]] `elasticsearch.pingTimeout:` | Time in milliseconds to wait for {es} to respond to pings. @@ -217,18 +244,22 @@ on the {kib} index at startup. {kib} users still need to authenticate with | Enables use of interpreter in Visualize. *Default: `true`* | `kibana.defaultAppId:` - | *deprecated* This setting is deprecated and will get removed in Kibana 8.0. -Please use the `defaultRoute` advanced setting instead. -The default application to load. *Default: `"home"`* + | deprecated:[7.9.0,This setting will be removed in Kibana 8.0.] + Instead, use the <>. + + + The default application to load. *Default: `"home"`* |[[kibana-index]] `kibana.index:` - | *deprecated* This setting is deprecated and will be removed in 8.0. Multitenancy by changing -`kibana.index` will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes] -for more details. {kib} uses an index in {es} to store saved searches, visualizations, and -dashboards. {kib} creates a new index if the index doesn’t already exist. -If you configure a custom index, the name must be lowercase, and conform to the -{es} {ref}/indices-create-index.html[index name limitations]. -*Default: `".kibana"`* + | deprecated:[7.11.0,This setting will be removed in 8.0.] Multitenancy by + changing `kibana.index` will not be supported starting in 8.0. See + https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes] for more + details. + + + {kib} uses an index in {es} to store saved searches, visualizations, and + dashboards. {kib} creates a new index if the index doesn’t already exist. If + you configure a custom index, the name must be lowercase, and conform to the + {es} {ref}/indices-create-index.html[index name limitations]. + *Default: `".kibana"`* | `kibana.autocompleteTimeout:` {ess-icon} | Time in milliseconds to wait for autocomplete suggestions from {es}. @@ -249,77 +280,44 @@ To reload the logging settings, send a SIGHUP signal to {kib}. [cols="2*<"] |=== -|[[logging-dest]] `logging.dest:` - | Enables you to specify a file where {kib} stores log output. -*Default: `stdout`* +|[[logging-root]] `logging.root:` +| The `root` logger has a dedicated configuration node since this context name is special and is pre-configured for logging by default. +// TODO: add link to the advanced logging documentation. -| `logging.json:` - | Logs output as JSON. When set to `true`, the logs are formatted as JSON -strings that include timestamp, log level, context, message text, and any other -metadata that may be associated with the log message. -When <> is set, and there is no interactive terminal ("TTY"), -this setting defaults to `true`. *Default: `false`* +|[[logging-root-appenders]] `logging.root.appenders:` +| A list of logging appenders to forward the root level logger instance to. By default `root` is configured with the `default` appender that must be included in the list. This is the configuration that all custom loggers will use unless they're re-configured explicitly. Additional appenders, if configured, can be included in the list. -| `logging.quiet:` - | Set the value of this setting to `true` to suppress all logging output other -than error messages. *Default: `false`* +|[[logging-root-level]] `logging.root.level:` {ess-icon} +| Level at which a log record should be logged. Supported levels are: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. Levels are ordered from _all_ (highest) to _off_ and a log record will be logged it its level is higher than or equal to the level of its logger, otherwise the log record is ignored. Use this value to <>. Set to `all` to log all events, including system usage information and all requests. Set to `off` to silence all logs. *Default: `info`*. -| `logging.rotate:` - | experimental[] Specifies the options for the logging rotate feature. -When not defined, all the sub options defaults would be applied. -The following example shows a valid logging rotate configuration: +|[[logging-loggers]] `logging.loggers:` + | Allows you to <>. -|=== +| `logging.loggers.name:` +| Specific logger instance. -[source,text] --- - logging.rotate: - enabled: true - everyBytes: 10485760 - keepFiles: 10 --- +| `logging.loggers.level:` +| Level at which a log record should be shown. Supported levels are: _all_, _fatal_, _error_, _warn_, _info_, _debug_, _trace_, _off_. -[cols="2*<"] -|=== +| `logging.loggers.appenders:` +| Specific appender format to apply for a particular logger context. + +| `logging.appenders:` +| Define how and where log messages are displayed (eg. *stdout* or console) and stored (eg. file on the disk). +// TODO: add link to the advanced logging documentation. + +| `logging.appenders.console:` +| Appender to use for logging records to *stdout*. By default, uses the `[%date][%level][%logger] %message` **pattern** layout. To use a **json**, set the <>. + +| `logging.appenders.file:` +| Allows you to specify a fileName to send log records to on disk. To send <>, add the file appender to `root.appenders`. + +| `logging.appenders.rolling-file:` +| Similar to Log4j's `RollingFileAppender`, this appender will log into a file and rotate if following a rolling strategy when the configured policy triggers. There are currently two policies supported: `size-limit` and `time-interval`. + +The size limit policy will perform a rollover when the log file reaches a maximum `size`. *Default 100mb* -| `logging.rotate.enabled:` - | experimental[] Set the value of this setting to `true` to -enable log rotation. If you do not have a <> set that is different from `stdout` -that feature would not take any effect. *Default: `false`* - -| `logging.rotate.everyBytes:` - | experimental[] The maximum size of a log file (that is `not an exact` limit). After the -limit is reached, a new log file is generated. The default size limit is 10485760 (10 MB) and -this option should be in the range of 1048576 (1 MB) to 1073741824 (1 GB). *Default: `10485760`* - -| `logging.rotate.keepFiles:` - | experimental[] The number of most recent rotated log files to keep -on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` -option has to be in the range of 2 to 1024 files. *Default: `7`* - -| `logging.rotate.pollingInterval:` - | experimental[] The number of milliseconds for the polling strategy in case -the <> is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* - -|[[logging-rotate-usePolling]] `logging.rotate.usePolling:` - | experimental[] By default we try to understand the best way to monitoring -the log file and warning about it. Please be aware there are some systems where watch api is not accurate. In those cases, in order to get the feature working, -the `polling` method could be used enabling that option. *Default: `false`* - -| `logging.silent:` - | Set the value of this setting to `true` to -suppress all logging output. *Default: `false`* - -| `logging.timezone` - | Set to the canonical time zone ID -(for example, `America/Los_Angeles`) to log events using that time zone. -For possible values, refer to -https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. -When not set, log events use the host timezone - -| [[logging-verbose]] `logging.verbose:` {ess-icon} - | Set to `true` to log all events, including system usage information and all -requests. *Default: `false`* +The time interval policy will rotate the log file every given interval of time. *Default 24h* | [[regionmap-ES-map]] `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. @@ -690,6 +688,7 @@ include::{kib-repo-dir}/settings/dev-settings.asciidoc[] include::{kib-repo-dir}/settings/graph-settings.asciidoc[] include::{kib-repo-dir}/settings/fleet-settings.asciidoc[] include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] +include::{kib-repo-dir}/settings/logging-settings.asciidoc[] include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/ml-settings.asciidoc[] diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 676c46368a6ee..80ce77f30c75e 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -4,17 +4,7 @@ Instead of using a visual editor to create charts, you define a graph by chaining functions together, using the *Timelion*-specific syntax. The syntax enables some features that classical point series charts don't offer, such as pulling data from different indices or data sources into one graph. -[NOTE] -==== -Timelion app deprecation - -*Timelion* is still supported, the *Timelion app* is deprecated in 7.0, replaced by -dashboard features. In 8.0 and later, the *Timelion app* is removed from {kib}. -To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. - -For information on how to migrate *Timelion app* worksheets, refer to the -link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]. -==== +deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In 8.0 and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] [float] ==== Timelion expressions diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index ebe095e0881b3..c43e9210dd7c8 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -126,10 +126,10 @@ all, the full logs from Reporting will be the first place to look. In `kibana.ym [source,yaml] -------------------------------------------------------------------------------- -logging.verbose: true +logging.root.level: all -------------------------------------------------------------------------------- -For more information about logging, see <>. +For more information about logging, see <>. [float] [[reporting-troubleshooting-puppeteer-debug-logs]] diff --git a/jest.config.js b/jest.config.js index 03dc832ba170c..bd1e865a7e64a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,6 @@ module.exports = { projects: [ '/packages/*/jest.config.js', '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', '/src/plugins/*/jest.config.js', '/test/*/jest.config.js', '/x-pack/plugins/*/jest.config.js', diff --git a/kibana.d.ts b/kibana.d.ts index a2c670c96a699..8a7a531890057 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -13,18 +13,3 @@ import * as Public from 'src/core/public'; import * as Server from 'src/core/server'; export { Public, Server }; - -/** - * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). - */ -import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; - -/** - * Re-export legacy types under a namespace. - */ -export namespace Legacy { - export type KibanaConfig = LegacyKibanaServer.KibanaConfig; - export type Request = LegacyKibanaServer.Request; - export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; - export type Server = LegacyKibanaServer.Server; -} diff --git a/package.json b/package.json index 2654c433ac5fa..f3846bd0f71a2 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", + "globby/fast-glob": "3.2.5", "**/graphql-toolkit/lodash": "^4.17.21", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-fetch/node-fetch": "^2.6.1", @@ -97,8 +98,8 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "26.0.0", - "@elastic/datemath": "link:packages/elastic-datemath", + "@elastic/charts": "27.0.0", + "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", "@elastic/eui": "31.10.0", @@ -240,7 +241,7 @@ "github-markdown-css": "^2.10.0", "glob": "^7.1.2", "glob-all": "^3.2.1", - "globby": "^8.0.1", + "globby": "^11.0.3", "graphql": "^0.13.2", "graphql-fields": "^1.0.2", "graphql-tag": "^2.10.3", @@ -409,10 +410,10 @@ "utility-types": "^3.10.0", "uuid": "3.3.2", "vega": "^5.19.1", - "vega-lite": "^4.17.0", + "vega-lite": "^5.0.0", "vega-schema-url-parser": "^2.1.0", "vega-spec-injector": "^0.0.2", - "vega-tooltip": "^0.25.0", + "vega-tooltip": "^0.25.1", "venn.js": "0.2.20", "vinyl": "^2.2.0", "vt-pbf": "^3.1.1", @@ -441,6 +442,7 @@ "@babel/traverse": "^7.12.12", "@babel/types": "^7.12.12", "@bazel/ibazel": "^0.14.0", + "@bazel/typescript": "^3.2.3", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", @@ -533,7 +535,6 @@ "@types/getos": "^3.0.0", "@types/git-url-parse": "^9.0.0", "@types/glob": "^7.1.2", - "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", "@types/gulp": "^4.0.6", "@types/gulp-zip": "^4.0.1", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 1f1eba0747ab7..31894fcb1bb5d 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -2,5 +2,7 @@ # targets so we can build them all at once filegroup( name = "build", - srcs = [], + srcs = [ + "//packages/elastic-datemath:build" + ], ) diff --git a/packages/elastic-datemath/.npmignore b/packages/elastic-datemath/.npmignore index 591be7afd1669..cb8c40d17ea04 100644 --- a/packages/elastic-datemath/.npmignore +++ b/packages/elastic-datemath/.npmignore @@ -1,2 +1,3 @@ +/index.test.js +/jest.config.js /tsconfig.json -/__tests__ diff --git a/packages/elastic-datemath/BUILD.bazel b/packages/elastic-datemath/BUILD.bazel new file mode 100644 index 0000000000000..6a80556d4eed5 --- /dev/null +++ b/packages/elastic-datemath/BUILD.bazel @@ -0,0 +1,76 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "elastic-datemath" +PKG_REQUIRE_NAME = "@elastic/datemath" + +SOURCE_FILES = [ + "src/index.ts", +] + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = glob(SOURCE_FILES), +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "@npm//moment", +] + +TYPES_DEPS = [ + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = [], + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + srcs = NPM_MODULE_EXTRA_FILES, + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/elastic-datemath/readme b/packages/elastic-datemath/README.md similarity index 100% rename from packages/elastic-datemath/readme rename to packages/elastic-datemath/README.md diff --git a/packages/elastic-datemath/index.d.ts b/packages/elastic-datemath/index.d.ts deleted file mode 100644 index 319c598e3e4ab..0000000000000 --- a/packages/elastic-datemath/index.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import moment from 'moment'; -export type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; - -declare const datemath: { - unitsMap: { - [k in Unit]: { - weight: number; - type: 'calendar' | 'fixed' | 'mixed'; - base: number; - }; - }; - units: Unit[]; - unitsAsc: Unit[]; - unitsDesc: Unit[]; - - /** - * Parses a string into a moment object. The string can be something like "now - 15m". - * @param options.forceNow If this optional parameter is supplied, "now" will be treated as this - * date, rather than the real "now". - */ - parse( - input: string, - options?: { - roundUp?: boolean; - forceNow?: Date; - momentInstance?: typeof moment; - } - ): moment.Moment | undefined; -}; - -// eslint-disable-next-line import/no-default-export -export default datemath; diff --git a/packages/elastic-datemath/package.json b/packages/elastic-datemath/package.json index 0d8f936ae6358..67fbb74eb223c 100644 --- a/packages/elastic-datemath/package.json +++ b/packages/elastic-datemath/package.json @@ -3,6 +3,9 @@ "version": "5.0.3", "description": "elasticsearch datemath parser, used in kibana", "license": "Apache-2.0", - "main": "index.js", - "typings": "index.d.ts" + "main": "./target/index.js", + "types": "./target/index.d.ts", + "peerDependencies": { + "moment": "^2.24.0" + } } \ No newline at end of file diff --git a/packages/elastic-datemath/index.test.js b/packages/elastic-datemath/src/index.test.js similarity index 100% rename from packages/elastic-datemath/index.test.js rename to packages/elastic-datemath/src/index.test.js diff --git a/packages/elastic-datemath/index.js b/packages/elastic-datemath/src/index.ts similarity index 71% rename from packages/elastic-datemath/index.js rename to packages/elastic-datemath/src/index.ts index 8a69d251d057d..a513af800b7c3 100644 --- a/packages/elastic-datemath/index.js +++ b/packages/elastic-datemath/src/index.ts @@ -17,9 +17,18 @@ * under the License. */ -const moment = require('moment'); +import moment from 'moment'; + +export type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; +export type UnitsMap = { + [k in Unit]: { + weight: number; + type: 'calendar' | 'fixed' | 'mixed'; + base: number; + }; +}; -const unitsMap = { +export const unitsMap: UnitsMap = { ms: { weight: 1, type: 'fixed', base: 1 }, s: { weight: 2, type: 'fixed', base: 1000 }, m: { weight: 3, type: 'mixed', base: 1000 * 60 }, @@ -30,13 +39,14 @@ const unitsMap = { // q: { weight: 8, type: 'calendar' }, // TODO: moment duration does not support quarter y: { weight: 9, type: 'calendar', base: NaN }, }; -const units = Object.keys(unitsMap).sort((a, b) => unitsMap[b].weight - unitsMap[a].weight); -const unitsDesc = [...units]; -const unitsAsc = [...units].reverse(); - -const isDate = (d) => Object.prototype.toString.call(d) === '[object Date]'; +export const units: Unit[] = Object.keys(unitsMap).sort( + (a, b) => unitsMap[b as Unit].weight - unitsMap[a as Unit].weight +) as Unit[]; +export const unitsDesc: Unit[] = [...units] as Unit[]; +export const unitsAsc: Unit[] = [...units].reverse() as Unit[]; -const isValidDate = (d) => isDate(d) && !isNaN(d.valueOf()); +const isDate = (d: string) => Object.prototype.toString.call(d) === '[object Date]'; +const isValidDate = (d: string) => isDate(d) && !isNaN(d.valueOf() as any); /* * This is a simplified version of elasticsearch's date parser. @@ -44,11 +54,17 @@ const isValidDate = (d) => isDate(d) && !isNaN(d.valueOf()); * will be done using this (and its locale settings) instead of the one bundled * with this library. */ -function parse(text, { roundUp = false, momentInstance = moment, forceNow } = {}) { +export function parse( + input: string, + options: { roundUp?: boolean; momentInstance?: typeof moment; forceNow?: Date } = {} +) { + const text = input; + const { roundUp = false, momentInstance = moment, forceNow } = options; + if (!text) return undefined; if (momentInstance.isMoment(text)) return text; if (isDate(text)) return momentInstance(text); - if (forceNow !== undefined && !isValidDate(forceNow)) { + if (forceNow !== undefined && !isValidDate(forceNow as any)) { throw new Error('forceNow must be a valid Date'); } @@ -80,7 +96,7 @@ function parse(text, { roundUp = false, momentInstance = moment, forceNow } = {} return parseDateMath(mathString, time, roundUp); } -function parseDateMath(mathString, time, roundUp) { +function parseDateMath(mathString: string, time: moment.Moment, roundUp: boolean) { const dateTime = time; const len = mathString.length; let i = 0; @@ -89,7 +105,7 @@ function parseDateMath(mathString, time, roundUp) { const c = mathString.charAt(i++); let type; let num; - let unit; + let unit: Unit; if (c === '/') { type = 0; @@ -101,13 +117,13 @@ function parseDateMath(mathString, time, roundUp) { return; } - if (isNaN(mathString.charAt(i))) { + if (isNaN(mathString.charAt(i) as any)) { num = 1; } else if (mathString.length === 2) { num = mathString.charAt(i); } else { const numFrom = i; - while (!isNaN(mathString.charAt(i))) { + while (!isNaN(mathString.charAt(i) as any)) { i++; if (i >= len) return; } @@ -121,7 +137,7 @@ function parseDateMath(mathString, time, roundUp) { } } - unit = mathString.charAt(i++); + unit = mathString.charAt(i++) as Unit; // append additional characters in the unit for (let j = i; j < len; j++) { @@ -138,12 +154,12 @@ function parseDateMath(mathString, time, roundUp) { return; } else { if (type === 0) { - if (roundUp) dateTime.endOf(unit); - else dateTime.startOf(unit); + if (roundUp) dateTime.endOf(unit as any); + else dateTime.startOf(unit as any); } else if (type === 1) { - dateTime.add(num, unit); + dateTime.add(num as any, unit); } else if (type === 2) { - dateTime.subtract(num, unit); + dateTime.subtract(num as any, unit); } } } @@ -151,8 +167,9 @@ function parseDateMath(mathString, time, roundUp) { return dateTime; } -module.exports = { - parse: parse, +// eslint-disable-next-line import/no-default-export +export default { + parse, unitsMap: Object.freeze(unitsMap), units: Object.freeze(units), unitsAsc: Object.freeze(unitsAsc), diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index cbfe1e8047433..d0fa806ed411b 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -1,9 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/elastic-datemath" + "declaration": true, + "declarationMap": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/elastic-datemath/src", + "types": [ + "node" + ] }, "include": [ - "index.d.ts" + "src/index.ts" ] } diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json index f7ca76b35e7c2..30f37b4786f36 100644 --- a/packages/kbn-ace/package.json +++ b/packages/kbn-ace/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "main": "./target/index.js", + "types": "./target/index.d.ts", "license": "SSPL-1.0 OR Elastic License 2.0", "scripts": { "build": "node ./scripts/build.js", diff --git a/packages/kbn-ace/tsconfig.json b/packages/kbn-ace/tsconfig.json index 6d3f433c6a6d1..9eef1ec56c6a2 100644 --- a/packages/kbn-ace/tsconfig.json +++ b/packages/kbn-ace/tsconfig.json @@ -1,13 +1,15 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "./target", "declaration": true, + "declarationMap": true, "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-ace/src", "types": [ - "jest", "node" - ] + ], }, "include": [ "src/**/*" diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index 861e0204a31a2..c2e579e7fdbea 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -1,20 +1,19 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, + "incremental": false, "outDir": "./target/types", "stripInternal": true, + "emitDeclarationOnly": true, + "declaration": true, "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-analytics/src", "types": [ - "jest", "node" ] }, "include": [ "src/**/*" - ], - "exclude": [ - "target" ] } diff --git a/packages/kbn-apm-config-loader/__fixtures__/config.yml b/packages/kbn-apm-config-loader/src/__fixtures__/config.yml similarity index 100% rename from packages/kbn-apm-config-loader/__fixtures__/config.yml rename to packages/kbn-apm-config-loader/src/__fixtures__/config.yml diff --git a/packages/kbn-apm-config-loader/__fixtures__/config_flat.yml b/packages/kbn-apm-config-loader/src/__fixtures__/config_flat.yml similarity index 100% rename from packages/kbn-apm-config-loader/__fixtures__/config_flat.yml rename to packages/kbn-apm-config-loader/src/__fixtures__/config_flat.yml diff --git a/packages/kbn-apm-config-loader/__fixtures__/en_var_ref_config.yml b/packages/kbn-apm-config-loader/src/__fixtures__/en_var_ref_config.yml similarity index 100% rename from packages/kbn-apm-config-loader/__fixtures__/en_var_ref_config.yml rename to packages/kbn-apm-config-loader/src/__fixtures__/en_var_ref_config.yml diff --git a/packages/kbn-apm-config-loader/__fixtures__/one.yml b/packages/kbn-apm-config-loader/src/__fixtures__/one.yml similarity index 100% rename from packages/kbn-apm-config-loader/__fixtures__/one.yml rename to packages/kbn-apm-config-loader/src/__fixtures__/one.yml diff --git a/packages/kbn-apm-config-loader/__fixtures__/two.yml b/packages/kbn-apm-config-loader/src/__fixtures__/two.yml similarity index 100% rename from packages/kbn-apm-config-loader/__fixtures__/two.yml rename to packages/kbn-apm-config-loader/src/__fixtures__/two.yml diff --git a/packages/kbn-apm-config-loader/src/utils/read_config.test.ts b/packages/kbn-apm-config-loader/src/utils/read_config.test.ts index 16fbb5ce7aed8..2838738c0ab6c 100644 --- a/packages/kbn-apm-config-loader/src/utils/read_config.test.ts +++ b/packages/kbn-apm-config-loader/src/utils/read_config.test.ts @@ -9,7 +9,7 @@ import { relative, resolve } from 'path'; import { getConfigFromFiles } from './read_config'; -const fixtureFile = (name: string) => resolve(__dirname, '..', '..', '__fixtures__', name); +const fixtureFile = (name: string) => resolve(__dirname, '..', '__fixtures__', name); test('reads single yaml from file system and parses to json', () => { const config = getConfigFromFiles([fixtureFile('config.yml')]); diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json index ba00ddfa6adb6..250195785b931 100644 --- a/packages/kbn-apm-config-loader/tsconfig.json +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -1,12 +1,19 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, + "incremental": false, "outDir": "./target", "stripInternal": false, + "declaration": true, "declarationMap": true, - "types": ["jest", "node"] + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-apm-config-loader/src", + "types": [ + "jest", + "node" + ] }, - "include": ["./src/**/*.ts"], - "exclude": ["target"] + "include": [ + "src/**/*.ts" + ] } diff --git a/packages/kbn-apm-utils/tsconfig.json b/packages/kbn-apm-utils/tsconfig.json index e1f79b5ef394d..e08769aab6543 100644 --- a/packages/kbn-apm-utils/tsconfig.json +++ b/packages/kbn-apm-utils/tsconfig.json @@ -1,18 +1,18 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, + "incremental": false, "outDir": "./target", "stripInternal": false, + "declaration": true, "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-apm-utils/src", "types": [ "node" ] }, "include": [ "./src/**/*.ts" - ], - "exclude": [ - "target" ] } diff --git a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts index 40841c8327cc2..6d12d5d05f07c 100644 --- a/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts +++ b/packages/kbn-cli-dev-mode/src/base_path_proxy_server.ts @@ -63,8 +63,6 @@ export class BasePathProxyServer { } public async start(options: BasePathProxyServerOptions) { - this.log.write('starting basepath proxy server'); - const serverOptions = getServerOptions(this.httpConfig); const listenerOptions = getListenerOptions(this.httpConfig); this.server = createServer(serverOptions, listenerOptions); @@ -101,7 +99,6 @@ export class BasePathProxyServer { return; } - this.log.write('stopping basepath proxy server'); await this.server.stop(); this.server = undefined; 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 d5bafe7280bd9..7b45a2639c668 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 @@ -135,6 +135,7 @@ it('passes correct args to sub-classes', () => { "repoRoot": , "runExamples": false, "silent": false, + "verbose": false, "watch": true, }, ], 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 94dbcb9654e8a..e867a7276989c 100644 --- a/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts +++ b/packages/kbn-cli-dev-mode/src/cli_dev_mode.ts @@ -7,6 +7,8 @@ */ import Path from 'path'; +import { EventEmitter } from 'events'; + import * as Rx from 'rxjs'; import { map, @@ -17,6 +19,7 @@ import { distinctUntilChanged, switchMap, concatMap, + takeUntil, } from 'rxjs/operators'; import { CliArgs } from '@kbn/config'; import { REPO_ROOT, CiStatsReporter } from '@kbn/dev-utils'; @@ -30,6 +33,16 @@ import { shouldRedirectFromOldBasePath } from './should_redirect_from_old_base_p import { getServerWatchPaths } from './get_server_watch_paths'; import { CliDevConfig } from './config'; +// signal that emits undefined once a termination signal has been sent +const exitSignal$ = new Rx.ReplaySubject(1); +Rx.merge( + Rx.fromEvent(process as EventEmitter, 'exit'), + Rx.fromEvent(process as EventEmitter, 'SIGINT'), + Rx.fromEvent(process as EventEmitter, 'SIGTERM') +) + .pipe(mapTo(undefined), take(1)) + .subscribe(exitSignal$); + // timeout where the server is allowed to exit gracefully const GRACEFUL_TIMEOUT = 5000; @@ -37,6 +50,7 @@ export type SomeCliArgs = Pick< CliArgs, | 'quiet' | 'silent' + | 'verbose' | 'disableOptimizer' | 'watch' | 'oss' @@ -148,6 +162,7 @@ export class CliDevMode { dist: cliArgs.dist, quiet: !!cliArgs.quiet, silent: !!cliArgs.silent, + verbose: !!cliArgs.verbose, watch: cliArgs.watch, }); } @@ -216,9 +231,36 @@ export class CliDevMode { this.log.warn('no-base-path', '='.repeat(100)); } - this.subscription.add(this.optimizer.run$.subscribe(this.observer('@kbn/optimizer'))); - this.subscription.add(this.watcher.run$.subscribe(this.observer('watcher'))); - this.subscription.add(this.devServer.run$.subscribe(this.observer('dev server'))); + this.subscription.add( + this.optimizer.run$ + .pipe( + // stop the optimizer as soon as we get an exit signal + takeUntil(exitSignal$) + ) + .subscribe(this.observer('@kbn/optimizer')) + ); + + this.subscription.add( + this.watcher.run$ + .pipe( + // stop the watcher as soon as we get an exit signal + takeUntil(exitSignal$) + ) + .subscribe(this.observer('watcher')) + ); + + this.subscription.add( + this.devServer.run$ + .pipe( + tap({ + complete: () => { + // when the devServer gracefully exits because of an exit signal stop the cli dev mode to trigger full shutdown + this.stop(); + }, + }) + ) + .subscribe(this.observer('dev server')) + ); } private reportTimings(reporter: CiStatsReporter) { diff --git a/packages/kbn-cli-dev-mode/src/config/load_config.ts b/packages/kbn-cli-dev-mode/src/config/load_config.ts index 46129834ca2d9..073cd3dbd4b4c 100644 --- a/packages/kbn-cli-dev-mode/src/config/load_config.ts +++ b/packages/kbn-cli-dev-mode/src/config/load_config.ts @@ -28,13 +28,13 @@ export const loadConfig = async ({ const configService = new ConfigService(rawConfigService, env, logger); configService.setSchema('dev', devConfigSchema); configService.setSchema('plugins', pluginsConfigSchema); - configService.setSchema('http', httpConfigSchema); + configService.setSchema('server', httpConfigSchema); await configService.validate(); const devConfig = configService.atPathSync('dev'); const pluginsConfig = configService.atPathSync('plugins'); - const httpConfig = configService.atPathSync('http'); + const httpConfig = configService.atPathSync('server'); return { dev: new DevConfig(devConfig), diff --git a/packages/kbn-cli-dev-mode/src/dev_server.ts b/packages/kbn-cli-dev-mode/src/dev_server.ts index 3daf298c82324..60a279e456e3d 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.ts @@ -249,5 +249,11 @@ export class DevServer { ) .subscribe(subscriber) ); + + // complete state subjects when run$ completes + subscriber.add(() => { + this.phase$.complete(); + this.ready$.complete(); + }); }); } 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 ab113b96a5f03..ff25f2a7bf55e 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 @@ -27,8 +27,6 @@ it('produces the right watch and ignore list', () => { expect(watchPaths).toMatchInlineSnapshot(` Array [ /src/core, - /src/legacy/server, - /src/legacy/utils, /config, /x-pack/test/plugin_functional/plugins/resolver_test, /src/plugins, diff --git a/packages/kbn-cli-dev-mode/src/optimizer.test.ts b/packages/kbn-cli-dev-mode/src/optimizer.test.ts index c270a00329897..ee8ea5f38ae84 100644 --- a/packages/kbn-cli-dev-mode/src/optimizer.test.ts +++ b/packages/kbn-cli-dev-mode/src/optimizer.test.ts @@ -46,6 +46,7 @@ const defaultOptions: Options = { pluginScanDirs: ['/some-scan-path'], quiet: true, silent: true, + verbose: false, repoRoot: '/app', runExamples: true, watch: true, @@ -179,6 +180,7 @@ it('is ready when optimizer phase is success or issue and logs in familiar forma "ready: false", "", "ready: true", + "complete", ] `); diff --git a/packages/kbn-cli-dev-mode/src/optimizer.ts b/packages/kbn-cli-dev-mode/src/optimizer.ts index 5e2f16fcf7daa..fab566829f7a6 100644 --- a/packages/kbn-cli-dev-mode/src/optimizer.ts +++ b/packages/kbn-cli-dev-mode/src/optimizer.ts @@ -25,6 +25,7 @@ export interface Options { repoRoot: string; quiet: boolean; silent: boolean; + verbose: boolean; watch: boolean; cache: boolean; dist: boolean; @@ -80,6 +81,7 @@ export class Optimizer { const { flags: levelFlags } = parseLogLevel( pickLevelFromFlags({ + verbose: options.verbose, quiet: options.quiet, silent: options.silent, }) @@ -105,14 +107,26 @@ export class Optimizer { }, ]); - this.run$ = runOptimizer(config).pipe( - logOptimizerState(log, config), - tap(({ state }) => { - this.phase$.next(state.phase); - this.ready$.next(state.phase === 'success' || state.phase === 'issue'); - }), - ignoreElements() - ); + this.run$ = new Rx.Observable((subscriber) => { + subscriber.add( + runOptimizer(config) + .pipe( + logOptimizerState(log, config), + tap(({ state }) => { + this.phase$.next(state.phase); + this.ready$.next(state.phase === 'success' || state.phase === 'issue'); + }), + ignoreElements() + ) + .subscribe(subscriber) + ); + + // complete state subjects when run$ completes + subscriber.add(() => { + this.phase$.complete(); + this.ready$.complete(); + }); + }); } getPhase$() { diff --git a/packages/kbn-cli-dev-mode/src/watcher.ts b/packages/kbn-cli-dev-mode/src/watcher.ts index 8e8d2db1b20bb..17993326cfcf3 100644 --- a/packages/kbn-cli-dev-mode/src/watcher.ts +++ b/packages/kbn-cli-dev-mode/src/watcher.ts @@ -103,6 +103,11 @@ export class Watcher { .pipe(ignoreElements()) .subscribe(subscriber) ); + + // complete state subjects when run$ completes + subscriber.add(() => { + this.restart$.complete(); + }); }); serverShouldRestart$() { diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json index b2bdaf8ceea36..4436d27dbff88 100644 --- a/packages/kbn-cli-dev-mode/tsconfig.json +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -1,11 +1,18 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, + "incremental": false, "outDir": "./target", + "declaration": true, "declarationMap": true, - "types": ["jest", "node"] + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-cli-dev-mode/src", + "types": [ + "jest", + "node" + ] }, - "include": ["./src/**/*.ts"], - "exclude": ["target"] + "include": [ + "./src/**/*.ts" + ], } diff --git a/packages/kbn-config-schema/tsconfig.json b/packages/kbn-config-schema/tsconfig.json index 6a268f2e7c016..d33683acded16 100644 --- a/packages/kbn-config-schema/tsconfig.json +++ b/packages/kbn-config-schema/tsconfig.json @@ -1,21 +1,21 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, - "declarationDir": "./target/types", + "incremental": false, "outDir": "./target/out", + "declarationDir": "./target/types", "stripInternal": true, + "declaration": true, "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-config-schema/src", "types": [ "jest", "node" ] }, "include": [ - "./types/joi.d.ts", - "./src/**/*.ts" - ], - "exclude": [ - "target" + "types/joi.d.ts", + "src/**/*.ts" ] } diff --git a/packages/kbn-config/__fixtures__/config.yml b/packages/kbn-config/src/__fixtures__/config.yml similarity index 100% rename from packages/kbn-config/__fixtures__/config.yml rename to packages/kbn-config/src/__fixtures__/config.yml diff --git a/packages/kbn-config/__fixtures__/config_flat.yml b/packages/kbn-config/src/__fixtures__/config_flat.yml similarity index 100% rename from packages/kbn-config/__fixtures__/config_flat.yml rename to packages/kbn-config/src/__fixtures__/config_flat.yml diff --git a/packages/kbn-config/__fixtures__/en_var_ref_config.yml b/packages/kbn-config/src/__fixtures__/en_var_ref_config.yml similarity index 100% rename from packages/kbn-config/__fixtures__/en_var_ref_config.yml rename to packages/kbn-config/src/__fixtures__/en_var_ref_config.yml diff --git a/packages/kbn-config/__fixtures__/one.yml b/packages/kbn-config/src/__fixtures__/one.yml similarity index 100% rename from packages/kbn-config/__fixtures__/one.yml rename to packages/kbn-config/src/__fixtures__/one.yml diff --git a/packages/kbn-config/__fixtures__/two.yml b/packages/kbn-config/src/__fixtures__/two.yml similarity index 100% rename from packages/kbn-config/__fixtures__/two.yml rename to packages/kbn-config/src/__fixtures__/two.yml diff --git a/packages/kbn-config/src/config_service.mock.ts b/packages/kbn-config/src/config_service.mock.ts index 638627caf1e50..83fbf20b5c0b3 100644 --- a/packages/kbn-config/src/config_service.mock.ts +++ b/packages/kbn-config/src/config_service.mock.ts @@ -25,13 +25,16 @@ const createConfigServiceMock = ({ setSchema: jest.fn(), addDeprecationProvider: jest.fn(), validate: jest.fn(), + getHandledDeprecatedConfigs: jest.fn(), }; + mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); mocked.atPathSync.mockReturnValue(atPath); mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); mocked.getUsedPaths.mockResolvedValue([]); mocked.getUnusedPaths.mockResolvedValue([]); mocked.isEnabledAtPath.mockResolvedValue(true); + mocked.getHandledDeprecatedConfigs.mockReturnValue([]); return mocked; }; diff --git a/packages/kbn-config/src/config_service.test.mocks.ts b/packages/kbn-config/src/config_service.test.mocks.ts index 99539726c3e43..d8da2852b9251 100644 --- a/packages/kbn-config/src/config_service.test.mocks.ts +++ b/packages/kbn-config/src/config_service.test.mocks.ts @@ -7,9 +7,15 @@ */ export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); +import type { applyDeprecations } from './deprecation/apply_deprecations'; + jest.mock('../../../package.json', () => mockPackage); -export const mockApplyDeprecations = jest.fn((config, deprecations, log) => config); +export const mockApplyDeprecations = jest.fn< + Record, + Parameters +>((config, deprecations, createAddDeprecation) => config); + jest.mock('./deprecation/apply_deprecations', () => ({ applyDeprecations: mockApplyDeprecations, })); diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index e38fff866df89..64404341bc64d 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -72,10 +72,10 @@ test('throws if config at path does not match schema', async () => { ); await expect(valuesReceived).toMatchInlineSnapshot(` - Array [ - [Error: [config validation of [key]]: expected value of type [string] but got [number]], - ] - `); + Array [ + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] + `); }); test('re-validate config when updated', async () => { @@ -97,11 +97,11 @@ test('re-validate config when updated', async () => { rawConfig$.next({ key: 123 }); - await expect(valuesReceived).toMatchInlineSnapshot(` - Array [ - "value", - [Error: [config validation of [key]]: expected value of type [string] but got [number]], - ] + expect(valuesReceived).toMatchInlineSnapshot(` + Array [ + "value", + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] `); }); @@ -416,10 +416,10 @@ test('throws during validation is any schema is invalid', async () => { test('logs deprecation warning during validation', async () => { const rawConfig = getRawConfigProvider({}); const configService = new ConfigService(rawConfig, defaultEnv, logger); - - mockApplyDeprecations.mockImplementationOnce((config, deprecations, log) => { - log('some deprecation message'); - log('another deprecation message'); + mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { + const addDeprecation = createAddDeprecation!(''); + addDeprecation({ message: 'some deprecation message' }); + addDeprecation({ message: 'another deprecation message' }); return config; }); @@ -437,6 +437,37 @@ test('logs deprecation warning during validation', async () => { `); }); +test('does not log warnings for silent deprecations during validation', async () => { + const rawConfig = getRawConfigProvider({}); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + + mockApplyDeprecations + .mockImplementationOnce((config, deprecations, createAddDeprecation) => { + const addDeprecation = createAddDeprecation!(''); + addDeprecation({ message: 'some deprecation message', silent: true }); + addDeprecation({ message: 'another deprecation message' }); + return config; + }) + .mockImplementationOnce((config, deprecations, createAddDeprecation) => { + const addDeprecation = createAddDeprecation!(''); + addDeprecation({ message: 'I am silent', silent: true }); + return config; + }); + + loggerMock.clear(logger); + await configService.validate(); + expect(loggerMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "another deprecation message", + ], + ] + `); + loggerMock.clear(logger); + await configService.validate(); + expect(loggerMock.collect(logger).warn).toMatchInlineSnapshot(`Array []`); +}); + describe('atPathSync', () => { test('returns the value at path', async () => { const rawConfig = getRawConfigProvider({ key: 'foo' }); @@ -477,3 +508,36 @@ describe('atPathSync', () => { expect(configService.atPathSync('key')).toEqual('new-value'); }); }); + +describe('getHandledDeprecatedConfigs', () => { + it('returns all handled deprecated configs', async () => { + const rawConfig = getRawConfigProvider({ base: { unused: 'unusedConfig' } }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + + configService.addDeprecationProvider('base', ({ unused }) => [unused('unused')]); + + mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { + deprecations.forEach((deprecation) => { + const addDeprecation = createAddDeprecation!(deprecation.path); + addDeprecation({ message: `some deprecation message`, documentationUrl: 'some-url' }); + }); + return config; + }); + + await configService.validate(); + + expect(configService.getHandledDeprecatedConfigs()).toMatchInlineSnapshot(` + Array [ + Array [ + "base", + Array [ + Object { + "documentationUrl": "some-url", + "message": "some deprecation message", + }, + ], + ], + ] + `); + }); +}); diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index d71327350d212..91927b4c7b5c9 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -21,6 +21,7 @@ import { ConfigDeprecationWithContext, ConfigDeprecationProvider, configDeprecationFactory, + DeprecatedConfigDetails, } from './deprecation'; import { LegacyObjectToConfigAdapter } from './legacy'; @@ -43,6 +44,7 @@ export class ConfigService { private readonly handledPaths: Set = new Set(); private readonly schemas = new Map>(); private readonly deprecations = new BehaviorSubject([]); + private readonly handledDeprecatedConfigs = new Map(); constructor( private readonly rawConfigProvider: RawConfigurationProvider, @@ -91,6 +93,13 @@ export class ConfigService { ]); } + /** + * returns all handled deprecated configs + */ + public getHandledDeprecatedConfigs() { + return [...this.handledDeprecatedConfigs.entries()]; + } + /** * Validate the whole configuration and log the deprecation warnings. * @@ -186,8 +195,16 @@ export class ConfigService { const rawConfig = await this.rawConfigProvider.getConfig$().pipe(take(1)).toPromise(); const deprecations = await this.deprecations.pipe(take(1)).toPromise(); const deprecationMessages: string[] = []; - const logger = (msg: string) => deprecationMessages.push(msg); - applyDeprecations(rawConfig, deprecations, logger); + const createAddDeprecation = (domainId: string) => (context: DeprecatedConfigDetails) => { + if (!context.silent) { + deprecationMessages.push(context.message); + } + const handledDeprecatedConfig = this.handledDeprecatedConfigs.get(domainId) || []; + handledDeprecatedConfig.push(context); + this.handledDeprecatedConfigs.set(domainId, handledDeprecatedConfig); + }; + + applyDeprecations(rawConfig, deprecations, createAddDeprecation); deprecationMessages.forEach((msg) => { this.deprecationLog.warn(msg); }); diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts index 9e058faf68052..f2c0a43916343 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts @@ -32,8 +32,31 @@ describe('applyDeprecations', () => { expect(handlerC).toHaveBeenCalledTimes(1); }); + it('passes path to addDeprecation factory', () => { + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + const alteredConfig = { foo: 'bar' }; + + const handlerA = jest.fn().mockReturnValue(alteredConfig); + const handlerB = jest.fn().mockImplementation((conf) => conf); + + applyDeprecations( + initialConfig, + [wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')], + createAddDeprecation + ); + + expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation); + expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation); + expect(createAddDeprecation).toBeCalledTimes(2); + expect(createAddDeprecation).toHaveBeenNthCalledWith(1, 'pathA'); + expect(createAddDeprecation).toHaveBeenNthCalledWith(2, 'pathB'); + }); + it('calls handlers with correct arguments', () => { - const logger = () => undefined; + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; const alteredConfig = { foo: 'bar' }; @@ -43,11 +66,11 @@ describe('applyDeprecations', () => { applyDeprecations( initialConfig, [wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')], - logger + createAddDeprecation ); - expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', logger); - expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', logger); + expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', addDeprecation); + expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', addDeprecation); }); it('returns the migrated config', () => { diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.ts b/packages/kbn-config/src/deprecation/apply_deprecations.ts index 0813440adb57c..6aced541dc30d 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.ts @@ -7,23 +7,24 @@ */ import { cloneDeep } from 'lodash'; -import { ConfigDeprecationWithContext, ConfigDeprecationLogger } from './types'; - -const noopLogger = (msg: string) => undefined; +import { ConfigDeprecationWithContext, AddConfigDeprecation } from './types'; +const noopAddDeprecationFactory: () => AddConfigDeprecation = () => () => undefined; /** - * Applies deprecations on given configuration and logs any deprecation warning using provided logger. + * Applies deprecations on given configuration and passes addDeprecation hook. + * This hook is used for logging any deprecation warning using provided logger. + * This hook is used for exposing deprecated configs that must be handled by the user before upgrading to next major. * * @internal */ export const applyDeprecations = ( config: Record, deprecations: ConfigDeprecationWithContext[], - logger: ConfigDeprecationLogger = noopLogger + createAddDeprecation: (pluginId: string) => AddConfigDeprecation = noopAddDeprecationFactory ) => { let processed = cloneDeep(config); deprecations.forEach(({ deprecation, path }) => { - processed = deprecation(processed, path, logger); + processed = deprecation(processed, path, createAddDeprecation(path)); }); return processed; }; diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index ba8a0cbf7ca57..11a49ed79d170 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -6,17 +6,16 @@ * Side Public License, v 1. */ -import { ConfigDeprecationLogger } from './types'; +import { DeprecatedConfigDetails } from './types'; import { configDeprecationFactory } from './deprecation_factory'; describe('DeprecationFactory', () => { const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; - let deprecationMessages: string[]; - const logger: ConfigDeprecationLogger = (msg) => deprecationMessages.push(msg); + const addDeprecation = jest.fn(); beforeEach(() => { - deprecationMessages = []; + addDeprecation.mockClear(); }); describe('rename', () => { @@ -30,7 +29,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { renamed: 'toberenamed', @@ -40,9 +39,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + }, + ], ] `); }); @@ -56,7 +64,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', logger); + const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { new: 'new', @@ -66,7 +74,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toHaveBeenCalledTimes(0); }); it('handles nested keys', () => { const rawConfig = { @@ -83,7 +91,7 @@ describe('DeprecationFactory', () => { const processed = rename('oldsection.deprecated', 'newsection.renamed')( rawConfig, 'myplugin', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { @@ -97,9 +105,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"myplugin.oldsection.deprecated\\" with \\"myplugin.newsection.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + }, + ], ] `); }); @@ -110,15 +127,25 @@ describe('DeprecationFactory', () => { renamed: 'renamed', }, }; - const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { renamed: 'renamed', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure \\"myplugin.renamed\\" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).", + "Remove \\"myplugin.deprecated\\" from the config.", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + }, + ], ] `); }); @@ -138,7 +165,7 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { @@ -149,9 +176,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + }, + ], ] `); }); @@ -169,7 +205,7 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ oldplugin: { @@ -180,9 +216,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Replace \\"oldplugin.deprecated\\" with \\"newplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", + ], + }, + "message": "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + }, + ], ] `); }); @@ -200,7 +245,7 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('myplugin.deprecated', 'myplugin.new')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { @@ -211,7 +256,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toBeCalledTimes(0); }); it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => { @@ -224,16 +269,27 @@ describe('DeprecationFactory', () => { const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( rawConfig, 'does-not-matter', - logger + addDeprecation ); expect(processed).toEqual({ myplugin: { renamed: 'renamed', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure \\"myplugin.renamed\\" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).", + "Remove \\"myplugin.deprecated\\" from the config.", + ], + }, + "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + }, + ], ] `); }); @@ -250,7 +306,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + const processed = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -259,9 +315,18 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "myplugin.deprecated is deprecated and is no longer used", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + ], + }, + "message": "myplugin.deprecated is deprecated and is no longer used", + }, + ], ] `); }); @@ -278,7 +343,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('section.deprecated')(rawConfig, 'myplugin', logger); + const processed = unused('section.deprecated')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -288,9 +353,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "myplugin.section.deprecated is deprecated and is no longer used", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + ], + }, + "message": "myplugin.section.deprecated is deprecated and is no longer used", + }, + ], ] `); }); @@ -304,7 +379,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + const processed = unused('deprecated')(rawConfig, 'myplugin', addDeprecation); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -313,7 +388,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toBeCalledTimes(0); }); }); @@ -328,7 +403,11 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + const processed = unusedFromRoot('myplugin.deprecated')( + rawConfig, + 'does-not-matter', + addDeprecation + ); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -337,9 +416,19 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages).toMatchInlineSnapshot(` + + expect(addDeprecation.mock.calls).toMatchInlineSnapshot(` Array [ - "myplugin.deprecated is deprecated and is no longer used", + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + ], + }, + "message": "myplugin.deprecated is deprecated and is no longer used", + }, + ], ] `); }); @@ -353,7 +442,11 @@ describe('DeprecationFactory', () => { property: 'value', }, }; - const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + const processed = unusedFromRoot('myplugin.deprecated')( + rawConfig, + 'does-not-matter', + addDeprecation + ); expect(processed).toEqual({ myplugin: { valid: 'valid', @@ -362,7 +455,7 @@ describe('DeprecationFactory', () => { property: 'value', }, }); - expect(deprecationMessages.length).toEqual(0); + expect(addDeprecation).toBeCalledTimes(0); }); }); }); diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 73196dc897a51..140846d86ae0b 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -9,15 +9,20 @@ import { get } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; import { unset } from '@kbn/std'; -import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types'; +import { + ConfigDeprecation, + AddConfigDeprecation, + ConfigDeprecationFactory, + DeprecatedConfigDetails, +} from './types'; const _rename = ( config: Record, rootPath: string, - log: ConfigDeprecationLogger, + addDeprecation: AddConfigDeprecation, oldKey: string, newKey: string, - silent?: boolean + details?: Partial ) => { const fullOldPath = getPath(rootPath, oldKey); const oldValue = get(config, fullOldPath); @@ -32,48 +37,80 @@ const _rename = ( if (newValue === undefined) { set(config, fullNewPath, oldValue); - if (!silent) { - log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); - } + addDeprecation({ + message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`, + correctiveActions: { + manualSteps: [ + `Replace "${fullOldPath}" with "${fullNewPath}" in the Kibana config file, CLI flag, or environment variable (in Docker only).`, + ], + }, + ...details, + }); } else { - if (!silent) { - log( - `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` - ); - } + addDeprecation({ + message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"`, + correctiveActions: { + manualSteps: [ + `Make sure "${fullNewPath}" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).`, + `Remove "${fullOldPath}" from the config.`, + ], + }, + ...details, + }); } + return config; }; const _unused = ( config: Record, rootPath: string, - log: ConfigDeprecationLogger, - unusedKey: string + addDeprecation: AddConfigDeprecation, + unusedKey: string, + details?: Partial ) => { const fullPath = getPath(rootPath, unusedKey); if (get(config, fullPath) === undefined) { return config; } unset(config, fullPath); - log(`${fullPath} is deprecated and is no longer used`); + addDeprecation({ + message: `${fullPath} is deprecated and is no longer used`, + correctiveActions: { + manualSteps: [ + `Remove "${fullPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only)`, + ], + }, + ...details, + }); return config; }; -const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => - _rename(config, rootPath, log, oldKey, newKey); +const rename = ( + oldKey: string, + newKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _rename(config, rootPath, addDeprecation, oldKey, newKey, details); -const renameFromRoot = (oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation => ( - config, - rootPath, - log -) => _rename(config, '', log, oldKey, newKey, silent); +const renameFromRoot = ( + oldKey: string, + newKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _rename(config, '', addDeprecation, oldKey, newKey, details); -const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => - _unused(config, rootPath, log, unusedKey); +const unused = ( + unusedKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _unused(config, rootPath, addDeprecation, unusedKey, details); -const unusedFromRoot = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => - _unused(config, '', log, unusedKey); +const unusedFromRoot = ( + unusedKey: string, + details?: Partial +): ConfigDeprecation => (config, rootPath, addDeprecation) => + _unused(config, '', addDeprecation, unusedKey, details); const getPath = (rootPath: string, subPath: string) => rootPath !== '' ? `${rootPath}.${subPath}` : subPath; diff --git a/packages/kbn-config/src/deprecation/index.ts b/packages/kbn-config/src/deprecation/index.ts index 6fe1a53efecbc..3286acca9e584 100644 --- a/packages/kbn-config/src/deprecation/index.ts +++ b/packages/kbn-config/src/deprecation/index.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ -export { +export type { ConfigDeprecation, ConfigDeprecationWithContext, - ConfigDeprecationLogger, ConfigDeprecationFactory, + AddConfigDeprecation, ConfigDeprecationProvider, + DeprecatedConfigDetails, } from './types'; export { configDeprecationFactory } from './deprecation_factory'; export { applyDeprecations } from './apply_deprecations'; diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 6e1816867abcf..3b1d004d7ec76 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -7,11 +7,33 @@ */ /** - * Logger interface used when invoking a {@link ConfigDeprecation} + * Config deprecation hook used when invoking a {@link ConfigDeprecation} * * @public */ -export type ConfigDeprecationLogger = (message: string) => void; +export type AddConfigDeprecation = (details: DeprecatedConfigDetails) => void; + +/** + * Deprecated Config Details + * + * @public + */ +export interface DeprecatedConfigDetails { + /* The message to be displayed for the deprecation. */ + message: string; + /* (optional) set false to prevent the config service from logging the deprecation message. */ + silent?: boolean; + /* (optional) link to the documentation for more details on the deprecation. */ + documentationUrl?: string; + /* (optional) corrective action needed to fix this deprecation. */ + correctiveActions?: { + /** + * Specify a list of manual steps our users need to follow + * to fix the deprecation before upgrade. + */ + manualSteps: string[]; + }; +} /** * Configuration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration. @@ -25,7 +47,7 @@ export type ConfigDeprecationLogger = (message: string) => void; export type ConfigDeprecation = ( config: Record, fromPath: string, - logger: ConfigDeprecationLogger + addDeprecation: AddConfigDeprecation ) => Record; /** @@ -62,6 +84,7 @@ export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => C * * @public */ + export interface ConfigDeprecationFactory { /** * Rename a configuration property from inside a plugin's configuration path. @@ -75,7 +98,11 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - rename(oldKey: string, newKey: string): ConfigDeprecation; + rename( + oldKey: string, + newKey: string, + details?: Partial + ): ConfigDeprecation; /** * Rename a configuration property from the root configuration. * Will log a deprecation warning if the oldKey was found and deprecation applied. @@ -91,7 +118,11 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; + renameFromRoot( + oldKey: string, + newKey: string, + details?: Partial + ): ConfigDeprecation; /** * Remove a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the unused key was found and deprecation applied. @@ -104,7 +135,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - unused(unusedKey: string): ConfigDeprecation; + unused(unusedKey: string, details?: Partial): ConfigDeprecation; /** * Remove a configuration property from the root configuration. * Will log a deprecation warning if the unused key was found and deprecation applied. @@ -120,7 +151,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - unusedFromRoot(unusedKey: string): ConfigDeprecation; + unusedFromRoot(unusedKey: string, details?: Partial): ConfigDeprecation; } /** @internal */ diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index c4845ab429c57..053bb93ce158c 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -24,6 +24,7 @@ export interface CliArgs { /** @deprecated */ quiet?: boolean; silent?: boolean; + verbose?: boolean; watch: boolean; basePath: boolean; oss: boolean; diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index 8b0bdb0befbfd..a9ea8265a3768 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -export { - applyDeprecations, - ConfigDeprecation, +export type { ConfigDeprecationFactory, - configDeprecationFactory, - ConfigDeprecationLogger, + AddConfigDeprecation, ConfigDeprecationProvider, ConfigDeprecationWithContext, + ConfigDeprecation, } from './deprecation'; +export { applyDeprecations, configDeprecationFactory } from './deprecation'; + export { RawConfigurationProvider, RawConfigService, diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 2801e0a0688cc..17ac75e9f3d9e 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -1,69 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get correctly handles server config.: default 1`] = ` -Object { - "autoListen": true, - "basePath": "/abc", - "compression": Object { - "enabled": true, - }, - "cors": false, - "customResponseHeaders": Object { - "custom-header": "custom-value", - }, - "host": "host", - "keepaliveTimeout": 5000, - "maxPayload": 1000, - "name": "kibana-hostname", - "port": 1234, - "publicBaseUrl": "https://myhost.com/abc", - "rewriteBasePath": false, - "socketTimeout": 2000, - "ssl": Object { - "enabled": true, - "keyPassphrase": "some-phrase", - "someNewValue": "new", - }, - "uuid": undefined, - "xsrf": Object { - "allowlist": Array [], - "disableProtection": false, - }, -} -`; - -exports[`#get correctly handles server config.: disabled ssl 1`] = ` -Object { - "autoListen": true, - "basePath": "/abc", - "compression": Object { - "enabled": true, - }, - "cors": false, - "customResponseHeaders": Object { - "custom-header": "custom-value", - }, - "host": "host", - "keepaliveTimeout": 5000, - "maxPayload": 1000, - "name": "kibana-hostname", - "port": 1234, - "publicBaseUrl": "http://myhost.com/abc", - "rewriteBasePath": false, - "socketTimeout": 2000, - "ssl": Object { - "certificate": "cert", - "enabled": false, - "key": "key", - }, - "uuid": undefined, - "xsrf": Object { - "allowlist": Array [], - "disableProtection": false, - }, -} -`; - exports[`#get correctly handles silent logging config. 1`] = ` Object { "appenders": Object { @@ -78,6 +14,7 @@ Object { "root": Object { "level": "off", }, + "silent": true, } `; @@ -93,10 +30,13 @@ Object { "type": "legacy-appender", }, }, + "dest": "/some/path.log", + "json": true, "loggers": undefined, "root": Object { "level": "all", }, + "verbose": true, } `; diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts index 5dd1941545708..47151503e1634 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.test.ts @@ -65,59 +65,6 @@ describe('#get', () => { expect(configAdapter.get('logging')).toMatchSnapshot(); }); - - test('correctly handles server config.', () => { - const configAdapter = new LegacyObjectToConfigAdapter({ - server: { - name: 'kibana-hostname', - autoListen: true, - basePath: '/abc', - cors: false, - customResponseHeaders: { 'custom-header': 'custom-value' }, - host: 'host', - maxPayloadBytes: 1000, - keepaliveTimeout: 5000, - socketTimeout: 2000, - port: 1234, - publicBaseUrl: 'https://myhost.com/abc', - rewriteBasePath: false, - ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' }, - compression: { enabled: true }, - someNotSupportedValue: 'val', - xsrf: { - disableProtection: false, - allowlist: [], - }, - }, - }); - - const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({ - server: { - name: 'kibana-hostname', - autoListen: true, - basePath: '/abc', - cors: false, - customResponseHeaders: { 'custom-header': 'custom-value' }, - host: 'host', - maxPayloadBytes: 1000, - keepaliveTimeout: 5000, - socketTimeout: 2000, - port: 1234, - publicBaseUrl: 'http://myhost.com/abc', - rewriteBasePath: false, - ssl: { enabled: false, certificate: 'cert', key: 'key' }, - compression: { enabled: true }, - someNotSupportedValue: 'val', - xsrf: { - disableProtection: false, - allowlist: [], - }, - }, - }); - - expect(configAdapter.get('server')).toMatchSnapshot('default'); - expect(configAdapterWithDisabledSSL.get('server')).toMatchSnapshot('disabled ssl'); - }); }); describe('#set', () => { diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index 8ec26ff1f8e71..bc6fd49e2498a 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -9,15 +9,6 @@ import { ConfigPath } from '../config'; import { ObjectToConfigAdapter } from '../object_to_config_adapter'; -// TODO: fix once core schemas are moved to this package -type LoggingConfigType = any; - -/** - * @internal - * @deprecated - */ -export type LegacyVars = Record; - /** * Represents logging config supported by the legacy platform. */ @@ -30,7 +21,7 @@ export interface LegacyLoggingConfig { events?: Record; } -type MixedLoggingConfig = LegacyLoggingConfig & Partial; +type MixedLoggingConfig = LegacyLoggingConfig & Record; /** * Represents adapter between config provided by legacy platform and `Config` @@ -48,6 +39,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { }, root: { level: 'info', ...root }, loggers, + ...legacyLoggingConfig, }; if (configValue.silent) { @@ -61,47 +53,11 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { return loggingConfig; } - private static transformServer(configValue: any = {}) { - // TODO: New platform uses just a subset of `server` config from the legacy platform, - // new values will be exposed once we need them - return { - autoListen: configValue.autoListen, - basePath: configValue.basePath, - cors: configValue.cors, - customResponseHeaders: configValue.customResponseHeaders, - host: configValue.host, - maxPayload: configValue.maxPayloadBytes, - name: configValue.name, - port: configValue.port, - publicBaseUrl: configValue.publicBaseUrl, - rewriteBasePath: configValue.rewriteBasePath, - ssl: configValue.ssl, - keepaliveTimeout: configValue.keepaliveTimeout, - socketTimeout: configValue.socketTimeout, - compression: configValue.compression, - uuid: configValue.uuid, - xsrf: configValue.xsrf, - }; - } - - private static transformPlugins(configValue: LegacyVars = {}) { - // These properties are the only ones we use from the existing `plugins` config node - // since `scanDirs` isn't respected by new platform plugin discovery. - return { - initialize: configValue.initialize, - paths: configValue.paths, - }; - } - public get(configPath: ConfigPath) { const configValue = super.get(configPath); switch (configPath) { case 'logging': return LegacyObjectToConfigAdapter.transformLogging(configValue as LegacyLoggingConfig); - case 'server': - return LegacyObjectToConfigAdapter.transformServer(configValue); - case 'plugins': - return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars); default: return configValue; } diff --git a/packages/kbn-config/src/raw/read_config.test.ts b/packages/kbn-config/src/raw/read_config.test.ts index 3b56c69098d2c..d428fa6b0a2a1 100644 --- a/packages/kbn-config/src/raw/read_config.test.ts +++ b/packages/kbn-config/src/raw/read_config.test.ts @@ -9,7 +9,7 @@ import { relative, resolve } from 'path'; import { getConfigFromFiles } from './read_config'; -const fixtureFile = (name: string) => resolve(`${__dirname}/../../__fixtures__/${name}`); +const fixtureFile = (name: string) => resolve(`${__dirname}/../__fixtures__/${name}`); test('reads single yaml from file system and parses to json', () => { const config = getConfigFromFiles([fixtureFile('config.yml')]); diff --git a/packages/kbn-config/tsconfig.json b/packages/kbn-config/tsconfig.json index ba00ddfa6adb6..4e1bf573f488a 100644 --- a/packages/kbn-config/tsconfig.json +++ b/packages/kbn-config/tsconfig.json @@ -1,12 +1,22 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, + "incremental": false, "outDir": "./target", "stripInternal": false, + "declaration": true, "declarationMap": true, - "types": ["jest", "node"] + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-config/src", + "types": [ + "jest", + "node" + ] }, - "include": ["./src/**/*.ts"], - "exclude": ["target"] + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] } diff --git a/packages/kbn-crypto/package.json b/packages/kbn-crypto/package.json index 6c7b3f3b0c719..7e26b96218319 100644 --- a/packages/kbn-crypto/package.json +++ b/packages/kbn-crypto/package.json @@ -4,6 +4,7 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "scripts": { "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index e9dd6313e6f79..5005152cac754 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -1,9 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "target", + "incremental": false, + "outDir": "./target", "declaration": true, - "declarationMap": true + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-crypto/src" }, "include": [ "src/**/*" diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index 7a2c3ce45a57d..e1990fca4e0bb 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -4,6 +4,7 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "scripts": { "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 1c6c671d0b768..65536c576b679 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,10 +1,18 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "target", + "stripInternal": false, "target": "ES2019", "declaration": true, - "declarationMap": true + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-dev-utils/src", + "types": [ + "jest", + "node" + ] }, "include": [ "src/**/*" diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 089732e9e6b40..26a7fa0e8c957 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -4,6 +4,7 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "private": "true", "main": "target/index.js", + "types": "target/index.d.ts", "kibana": { "devOnly": true }, diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 3c683f487b9f2..6f4a6fa2af8a5 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,10 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "./target", + "target": "ES2019", "declaration": true, + "declarationMap": true, "sourceMap": true, - "target": "ES2019" + "sourceRoot": "../../../../packages/kbn-docs-utils/src", + "types": [ + "jest", + "node" + ] }, "include": [ "src/**/*" diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 03ecee34be7e2..047d1dd675d26 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -4,6 +4,7 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "private": "true", "main": "target/index.js", + "types": "target/index.d.ts", "kibana": { "devOnly": true }, diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 02209a29e5817..0950cd39d0bee 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,10 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "./target", + "target": "ES2019", "declaration": true, + "declarationMap": true, "sourceMap": true, - "target": "ES2019" + "sourceRoot": "../../../../packages/kbn-es-archiver/src", + "types": [ + "jest", + "node" + ] }, "include": [ "src/**/*" diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index c6380f1cde969..9d4cb8c9b0972 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,5 +1,18 @@ { "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-i18n/src", + "types": [ + "jest", + "node" + ] + }, "include": [ "src/**/*.ts", "src/**/*.tsx", @@ -7,15 +20,6 @@ "types/intl_relativeformat.d.ts" ], "exclude": [ - "target" - ], - "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "outDir": "./target/types", - "types": [ - "jest", - "node" - ] - } + "**/__fixtures__/**/*" + ] } diff --git a/packages/kbn-interpreter/common/package.json b/packages/kbn-interpreter/common/package.json index b569e42220f04..62061138234d9 100644 --- a/packages/kbn-interpreter/common/package.json +++ b/packages/kbn-interpreter/common/package.json @@ -1,5 +1,6 @@ { "private": true, "main": "../target/common/index.js", + "types": "../target/common/index.d.ts", "jsnext:main": "../src/common/index.js" } \ No newline at end of file diff --git a/packages/kbn-legacy-logging/package.json b/packages/kbn-legacy-logging/package.json index 1e3752eca6755..96edeccad6658 100644 --- a/packages/kbn-legacy-logging/package.json +++ b/packages/kbn-legacy-logging/package.json @@ -4,12 +4,14 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "scripts": { "build": "tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, "dependencies": { - "@kbn/utils": "link:../kbn-utils" + "@kbn/utils": "link:../kbn-utils", + "@kbn/config-schema": "link:../kbn-config-schema" } } diff --git a/packages/kbn-legacy-logging/src/legacy_logging_server.ts b/packages/kbn-legacy-logging/src/legacy_logging_server.ts index e1edd06a4b4a2..3ece0f6f1ee47 100644 --- a/packages/kbn-legacy-logging/src/legacy_logging_server.ts +++ b/packages/kbn-legacy-logging/src/legacy_logging_server.ts @@ -88,7 +88,7 @@ export class LegacyLoggingServer { // We set `ops.interval` to max allowed number and `ops` filter to value // that doesn't exist to avoid logging of ops at all, if turned on it will be // logged by the "legacy" Kibana. - const { value: loggingConfig } = legacyLoggingConfigSchema.validate({ + const loggingConfig = legacyLoggingConfigSchema.validate({ ...legacyLoggingConfig, events: { ...legacyLoggingConfig.events, diff --git a/packages/kbn-legacy-logging/src/schema.ts b/packages/kbn-legacy-logging/src/schema.ts index 76d7381ee8728..0330708e746c0 100644 --- a/packages/kbn-legacy-logging/src/schema.ts +++ b/packages/kbn-legacy-logging/src/schema.ts @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import Joi from 'joi'; +import { schema } from '@kbn/config-schema'; -const HANDLED_IN_KIBANA_PLATFORM = Joi.any().description( - 'This key is handled in the new platform ONLY' -); /** * @deprecated * @@ -36,46 +33,65 @@ export interface LegacyLoggingConfig { }; } -export const legacyLoggingConfigSchema = Joi.object() - .keys({ - appenders: HANDLED_IN_KIBANA_PLATFORM, - loggers: HANDLED_IN_KIBANA_PLATFORM, - root: HANDLED_IN_KIBANA_PLATFORM, - - silent: Joi.boolean().default(false), - quiet: Joi.boolean().when('silent', { - is: true, - then: Joi.boolean().default(true).valid(true), - otherwise: Joi.boolean().default(false), +export const legacyLoggingConfigSchema = schema.object({ + silent: schema.boolean({ defaultValue: false }), + quiet: schema.conditional( + schema.siblingRef('silent'), + true, + schema.boolean({ + defaultValue: true, + validate: (quiet) => { + if (!quiet) { + return 'must be true when `silent` is true'; + } + }, + }), + schema.boolean({ defaultValue: false }) + ), + verbose: schema.conditional( + schema.siblingRef('quiet'), + true, + schema.boolean({ + defaultValue: false, + validate: (verbose) => { + if (verbose) { + return 'must be false when `quiet` is true'; + } + }, + }), + schema.boolean({ defaultValue: false }) + ), + events: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + dest: schema.string({ defaultValue: 'stdout' }), + filter: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + json: schema.conditional( + schema.siblingRef('dest'), + 'stdout', + schema.boolean({ + defaultValue: !process.stdout.isTTY, + }), + schema.boolean({ + defaultValue: true, + }) + ), + timezone: schema.maybe(schema.string()), + rotate: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + everyBytes: schema.number({ + min: 1048576, // > 1MB + max: 1073741825, // < 1GB + defaultValue: 10485760, // 10MB }), - verbose: Joi.boolean().when('quiet', { - is: true, - then: Joi.valid(false).default(false), - otherwise: Joi.boolean().default(false), + keepFiles: schema.number({ + min: 2, + max: 1024, + defaultValue: 7, }), - events: Joi.any().default({}), - dest: Joi.string().default('stdout'), - filter: Joi.any().default({}), - json: Joi.boolean().when('dest', { - is: 'stdout', - then: Joi.boolean().default(!process.stdout.isTTY), - otherwise: Joi.boolean().default(true), + pollingInterval: schema.number({ + min: 5000, + max: 3600000, + defaultValue: 10000, }), - timezone: Joi.string(), - rotate: Joi.object() - .keys({ - enabled: Joi.boolean().default(false), - everyBytes: Joi.number() - // > 1MB - .greater(1048576) - // < 1GB - .less(1073741825) - // 10MB - .default(10485760), - keepFiles: Joi.number().greater(2).less(1024).default(7), - pollingInterval: Joi.number().greater(5000).less(3600000).default(10000), - usePolling: Joi.boolean().default(false), - }) - .default(), - }) - .default(); + usePolling: schema.boolean({ defaultValue: false }), + }), +}); diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json index 8fd202a2dce8b..5f8d38ec90bcd 100644 --- a/packages/kbn-legacy-logging/tsconfig.json +++ b/packages/kbn-legacy-logging/tsconfig.json @@ -1,11 +1,19 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "target", "stripInternal": false, "declaration": true, "declarationMap": true, - "types": ["jest", "node"] + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-legacy-logging/src", + "types": [ + "jest", + "node" + ] }, - "include": ["./src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/packages/kbn-logging/package.json b/packages/kbn-logging/package.json index 8d3ffa09b083e..c7db148c75a2a 100644 --- a/packages/kbn-logging/package.json +++ b/packages/kbn-logging/package.json @@ -4,6 +4,7 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "scripts": { "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", diff --git a/packages/kbn-logging/tsconfig.json b/packages/kbn-logging/tsconfig.json index c55c05de30a52..adec4c1966036 100644 --- a/packages/kbn-logging/tsconfig.json +++ b/packages/kbn-logging/tsconfig.json @@ -1,11 +1,19 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "target", "stripInternal": false, "declaration": true, "declarationMap": true, - "types": ["jest", "node"] + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-logging/src", + "types": [ + "jest", + "node" + ] }, - "include": ["./src/**/*.ts"] + "include": [ + "src/**/*.ts" + ] } diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index e99661f8db598..bdf36915bab3a 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "private": true, "main": "./target/index.js", + "types": "./target/index.d.ts", "license": "SSPL-1.0 OR Elastic License 2.0", "scripts": { "build": "node ./scripts/build.js", diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 6d3f433c6a6d1..e6ec96b12c6cf 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -1,9 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "./target", "declaration": true, + "declarationMap": true, "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-monaco/src", "types": [ "jest", "node" diff --git a/packages/kbn-optimizer/index.d.ts b/packages/kbn-optimizer/index.d.ts deleted file mode 100644 index 004ac67f4b0c4..0000000000000 --- a/packages/kbn-optimizer/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './src/index'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f93849e011d41..a027768ad66a0 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -9,7 +9,7 @@ pageLoadAssetSize: charts: 195358 cloud: 21076 console: 46091 - core: 692106 + core: 397521 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 @@ -24,13 +24,13 @@ pageLoadAssetSize: enterpriseSearch: 35741 esUiShared: 326654 expressions: 224136 - features: 31211 - globalSearch: 43548 - globalSearchBar: 62888 + features: 21723 + globalSearch: 29696 + globalSearchBar: 50403 globalSearchProviders: 25554 graph: 31504 grokdebugger: 26779 - home: 41661 + home: 30182 indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 28222 @@ -45,13 +45,12 @@ pageLoadAssetSize: kibanaUtils: 198829 lens: 96624 licenseManagement: 41817 - licensing: 39008 + licensing: 29004 lists: 202261 logstash: 53548 management: 46112 - maps: 183610 + maps: 80000 mapsLegacy: 87859 - mapsLegacyLicensing: 20214 ml: 82187 monitoring: 80000 navigation: 37269 @@ -73,8 +72,8 @@ pageLoadAssetSize: share: 99061 snapshotRestore: 79032 spaces: 387915 - telemetry: 91832 - telemetryManagementSection: 52443 + telemetry: 51957 + telemetryManagementSection: 38586 tileMap: 65337 timelion: 29920 transform: 41007 @@ -108,3 +107,4 @@ pageLoadAssetSize: fileUpload: 25664 banners: 17946 mapsEms: 26072 + timelines: 28613 diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 29e33bf23424b..ac73fbc0fc16a 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -4,8 +4,9 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "scripts": { - "build": "../../node_modules/.bin/babel src --out-dir target --copy-files --delete-dir-on-start --extensions .ts --ignore *.test.ts --source-maps=inline", + "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 6e3106dbc2af7..d5b9996dfb2cd 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import 'source-map-support/register'; - import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index ff9aa6fd90628..9dbaae9f36f20 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -42,7 +42,6 @@ it('creates cache keys', () => { "id": "bar", "manifestPath": undefined, "outputDir": "/foo/bar/target", - "pageLoadAssetSizeLimit": undefined, "publicDirNames": Array [ "public", ], diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index 64b44de0dd1b3..08946deec0b47 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -11,6 +11,7 @@ import Fs from 'fs'; import { BundleCache } from './bundle_cache'; import { UnknownVals } from './ts_helpers'; +import { omit } from './obj_helpers'; import { includes, ascending, entriesToObject } from './array_helpers'; const VALID_BUNDLE_TYPES = ['plugin' as const, 'entry' as const]; @@ -90,7 +91,7 @@ export class Bundle { */ createCacheKey(files: string[], mtimes: Map): unknown { return { - spec: this.toSpec(), + spec: omit(this.toSpec(), ['pageLoadAssetSizeLimit']), mtimes: entriesToObject( files.map((p) => [p, mtimes.get(p)] as const).sort(ascending((e) => e[0])) ), diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 0f2c6a3517603..7914d74fa9299 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -18,3 +18,4 @@ export * from './array_helpers'; export * from './event_stream_helpers'; export * from './parse_path'; export * from './theme_tags'; +export * from './obj_helpers'; diff --git a/packages/kbn-pm/src/utils/bazel/ensure_yarn_integrity_exists.ts b/packages/kbn-optimizer/src/common/obj_helpers.ts similarity index 58% rename from packages/kbn-pm/src/utils/bazel/ensure_yarn_integrity_exists.ts rename to packages/kbn-optimizer/src/common/obj_helpers.ts index 90786bc0ea55e..f238eb22e93a1 100644 --- a/packages/kbn-pm/src/utils/bazel/ensure_yarn_integrity_exists.ts +++ b/packages/kbn-optimizer/src/common/obj_helpers.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ -import { join } from 'path'; -import { writeFile } from '../fs'; - -export async function ensureYarnIntegrityFileExists(nodeModulesPath: string) { - try { - await writeFile(join(nodeModulesPath, '.yarn-integrity'), '', { flag: 'wx' }); - } catch { - // no-op +export function omit(obj: T, keys: K[]): Omit { + const result: any = {}; + for (const [key, value] of Object.entries(obj) as any) { + if (!keys.includes(key)) { + result[key] = value; + } } + return result as Omit; } diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index a86f231b79806..50c9e7e12904f 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -16,7 +16,7 @@ import del from 'del'; import { tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog } from '@kbn/dev-utils'; -import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '@kbn/optimizer'; +import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '../index'; import { allValuesFrom } from '../common'; @@ -135,7 +135,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/lib.ts, - /packages/kbn-optimizer/target/worker/entry_point_creator.js, + /packages/kbn-optimizer/src/worker/entry_point_creator.ts, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -161,7 +161,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v8dark.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/core/public/core_app/styles/_globals_v8light.scss, - /packages/kbn-optimizer/target/worker/entry_point_creator.js, + /packages/kbn-optimizer/src/worker/entry_point_creator.ts, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); @@ -175,7 +175,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Array [ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, - /packages/kbn-optimizer/target/worker/entry_point_creator.js, + /packages/kbn-optimizer/src/worker/entry_point_creator.ts, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts index edf4545ae52b3..9f0ed9af556fb 100644 --- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -61,18 +61,26 @@ function usingWorkerProc( ) { return Rx.using( (): ProcResource => { - const proc = execa.node(require.resolve('../worker/run_worker'), [], { - nodeOptions: [ - ...process.execArgv, - ...(inspectFlag && config.inspectWorkers - ? [`${inspectFlag}=${inspectPortCounter++}`] - : []), - ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), - ], - buffer: false, - stderr: 'pipe', - stdout: 'pipe', - }); + const workerPath = require.resolve('../worker/run_worker'); + const proc = execa.node( + workerPath.endsWith('.ts') + ? require.resolve('../worker/run_worker_from_source') // workerFromSourcePath + : workerPath, + [], + { + nodeOptions: [ + '--preserve-symlinks', + '--preserve-symlinks-main', + ...(inspectFlag && config.inspectWorkers + ? [`${inspectFlag}=${inspectPortCounter++}`] + : []), + ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), + ], + buffer: false, + stderr: 'pipe', + stdout: 'pipe', + } + ); return { proc, diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index c546a0c6cf992..8becc76a23ca2 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -457,7 +457,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 22, + 25, ], "results": Array [ Object { @@ -480,7 +480,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 25, + 28, ], "results": Array [ Object { @@ -505,7 +505,7 @@ describe('OptimizerConfig::create()', () => { [Window], ], "invocationCallOrder": Array [ - 23, + 26, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 9110b6db27e92..2dbe48c15483f 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -17,6 +17,7 @@ import { ThemeTag, ThemeTags, parseThemeTags, + omit, } from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; @@ -40,16 +41,6 @@ function pickMaxWorkerCount(dist: boolean) { return Math.max(maxWorkers, 2); } -function omit(obj: T, keys: K[]): Omit { - const result: any = {}; - for (const [key, value] of Object.entries(obj) as any) { - if (!keys.includes(key)) { - result[key] = value; - } - } - return result as Omit; -} - interface Options { /** absolute path to root of the repo/build */ repoRoot: string; diff --git a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts index 563b4ecb4bc37..f7604f0f78f71 100644 --- a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts +++ b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts @@ -16,7 +16,6 @@ export class BundleRefModule extends Module { public built = false; public buildMeta?: any; public buildInfo?: any; - public exportsArgument = '__webpack_exports__'; constructor(public readonly ref: BundleRef) { super('kbn/bundleRef', null); @@ -45,7 +44,9 @@ export class BundleRefModule extends Module { build(_: any, __: any, ___: any, ____: any, callback: () => void) { this.built = true; this.buildMeta = {}; - this.buildInfo = {}; + this.buildInfo = { + exportsArgument: '__webpack_exports__', + }; callback(); } diff --git a/src/core/server/utils/index.ts b/packages/kbn-optimizer/src/worker/run_worker_from_source.js similarity index 80% rename from src/core/server/utils/index.ts rename to packages/kbn-optimizer/src/worker/run_worker_from_source.js index b0776c48f3bed..bebe984a447d6 100644 --- a/src/core/server/utils/index.ts +++ b/packages/kbn-optimizer/src/worker/run_worker_from_source.js @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export * from './from_root'; -export * from './package_json'; +require('@kbn/optimizer').registerNodeAutoTranspilation(); +require('./run_worker'); diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index 20b06b5658cbc..f2d508cf14a55 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -1,10 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-optimizer" + "incremental": false, + "outDir": "./target", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-optimizer/src" }, "include": [ - "index.d.ts", "src/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*" ] } diff --git a/packages/kbn-plugin-generator/package.json b/packages/kbn-plugin-generator/package.json index a0a18bfe7d1cb..ae4dfbc670f19 100644 --- a/packages/kbn-plugin-generator/package.json +++ b/packages/kbn-plugin-generator/package.json @@ -4,6 +4,7 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "target/index.js", + "types": "target/index.d.ts", "scripts": { "kbn:bootstrap": "node scripts/build", "kbn:watch": "node scripts/build --watch" diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index c54ff041d7065..5e885527a7608 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,12 +1,22 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "target", "target": "ES2019", "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-plugin-generator/src", + "types": [ + "jest", + "node" + ] }, - "include": ["src/**/*"], - "exclude": ["src/template/*"] + "include": [ + "src/**/*" + ], + "exclude": [ + "src/template/*" + ], } diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index cc845ef9d027f..6b9dd4d51baf9 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -8,6 +8,7 @@ "devOnly": true }, "main": "target/index.js", + "types": "target/index.d.ts", "bin": { "plugin-helpers": "bin/plugin-helpers.js" }, diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index 651bc79d6e707..87d11843f398a 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -1,10 +1,17 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "target", + "target": "ES2018", "declaration": true, + "declarationMap": true, "sourceMap": true, - "target": "ES2018" + "sourceRoot": "../../../../packages/kbn-plugin-helpers/src", + "types": [ + "jest", + "node" + ] }, "include": [ "src/**/*" diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 9bf332bf82319..7c5d0390d9fba 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -209,7 +209,7 @@ async function run(argv) { }, default: { cache: true, - 'force-install': true, + 'force-install': false, offline: false, validate: true }, @@ -550,7 +550,7 @@ Object.defineProperty(exports, "pickLevelFromFlags", { enumerable: true, get: fu Object.defineProperty(exports, "parseLogLevel", { enumerable: true, get: function () { return log_levels_1.parseLogLevel; } }); var tooling_log_collecting_writer_1 = __webpack_require__(127); Object.defineProperty(exports, "ToolingLogCollectingWriter", { enumerable: true, get: function () { return tooling_log_collecting_writer_1.ToolingLogCollectingWriter; } }); - +//# sourceMappingURL=index.js.map /***/ }), /* 6 */ @@ -628,7 +628,7 @@ class ToolingLog { } } exports.ToolingLog = ToolingLog; - +//# sourceMappingURL=tooling_log.js.map /***/ }), /* 7 */ @@ -6749,7 +6749,7 @@ class ToolingLogTextWriter { } } exports.ToolingLogTextWriter = ToolingLogTextWriter; - +//# sourceMappingURL=tooling_log_text_writer.js.map /***/ }), /* 112 */ @@ -8790,7 +8790,7 @@ function parseLogLevel(name) { }; } exports.parseLogLevel = parseLogLevel; - +//# sourceMappingURL=log_levels.js.map /***/ }), /* 127 */ @@ -8823,7 +8823,7 @@ class ToolingLogCollectingWriter extends tooling_log_text_writer_1.ToolingLogTex } } exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; - +//# sourceMappingURL=tooling_log_collecting_writer.js.map /***/ }), /* 128 */ @@ -8910,8 +8910,11 @@ const BootstrapCommand = { const nonBazelProjectsOnly = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_4__["getNonBazelProjectsOnly"])(projects); const batchedNonBazelProjects = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_4__["topologicallyBatchProjects"])(nonBazelProjectsOnly, projectGraph); const kibanaProjectPath = ((_projects$get = projects.get('kibana')) === null || _projects$get === void 0 ? void 0 : _projects$get.path) || ''; - const runOffline = (options === null || options === void 0 ? void 0 : options.offline) === true; - const forceInstall = !!options && options['force-install'] === true; // Ensure we have a `node_modules/.yarn-integrity` file as we depend on it + const runOffline = (options === null || options === void 0 ? void 0 : options.offline) === true; // Force install is set in case a flag is passed or + // if the `.yarn-integrity` file is not found which + // will be indicated by the return of yarnIntegrityFileExists. + + const forceInstall = !!options && options['force-install'] === true || !(await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["yarnIntegrityFileExists"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kibanaProjectPath, 'node_modules'))); // Ensure we have a `node_modules/.yarn-integrity` file as we depend on it // for bazel to know it has to re-install the node_modules after a reset or a clean await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["ensureYarnIntegrityFileExists"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(kibanaProjectPath, 'node_modules')); // Install bazel machinery tools if needed @@ -8925,9 +8928,6 @@ const BootstrapCommand = { // That way non bazel projects could depend on bazel projects but not the other way around // That is only intended during the migration process while non Bazel projects are not removed at all. // - // Until we have our first package build within Bazel we will always need to directly call the yarn rule - // otherwise yarn install won't trigger as we don't have any npm dependency within Bazel - // TODO: Change CLI default in order to not force install as soon as we have our first Bazel package being built if (forceInstall) { await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_9__["runBazel"])(['run', '@nodejs//:yarn'], runOffline); @@ -9105,6 +9105,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isDirectory", function() { return isDirectory; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isFile", function() { return isFile; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "createSymlink", function() { return createSymlink; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "tryRealpath", function() { return tryRealpath; }); /* harmony import */ var cmd_shim__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(132); /* harmony import */ var cmd_shim__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cmd_shim__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); @@ -9137,6 +9138,7 @@ const symlink = Object(util__WEBPACK_IMPORTED_MODULE_5__["promisify"])(fs__WEBPA const chmod = Object(util__WEBPACK_IMPORTED_MODULE_5__["promisify"])(fs__WEBPACK_IMPORTED_MODULE_2___default.a.chmod); const cmdShim = Object(util__WEBPACK_IMPORTED_MODULE_5__["promisify"])(cmd_shim__WEBPACK_IMPORTED_MODULE_0___default.a); const mkdir = Object(util__WEBPACK_IMPORTED_MODULE_5__["promisify"])(fs__WEBPACK_IMPORTED_MODULE_2___default.a.mkdir); +const realpathNative = Object(util__WEBPACK_IMPORTED_MODULE_5__["promisify"])(fs__WEBPACK_IMPORTED_MODULE_2___default.a.realpath.native); const mkdirp = async path => await mkdir(path, { recursive: true }); @@ -9220,6 +9222,20 @@ async function forceCreate(src, dest, type) { await symlink(src, dest, type); } +async function tryRealpath(path) { + let calculatedPath = path; + + try { + calculatedPath = await realpathNative(path); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + return calculatedPath; +} + /***/ }), /* 132 */ /***/ (function(module, exports, __webpack_require__) { @@ -14439,6 +14455,7 @@ module.exports = FastGlob; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; const utils = __webpack_require__(165); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); @@ -14510,6 +14527,7 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; const array = __webpack_require__(166); exports.array = array; const errno = __webpack_require__(167); @@ -14533,6 +14551,7 @@ exports.string = string; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.splitWhen = exports.flatten = void 0; function flatten(items) { return items.reduce((collection, item) => [].concat(collection, item), []); } @@ -14561,6 +14580,7 @@ exports.splitWhen = splitWhen; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEnoentCodeError = void 0; function isEnoentCodeError(error) { return error.code === 'ENOENT'; } @@ -14574,6 +14594,7 @@ exports.isEnoentCodeError = isEnoentCodeError; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDirentFromStats = void 0; class DirentFromStats { constructor(name, stats) { this.name = name; @@ -14599,6 +14620,7 @@ exports.createDirentFromStats = createDirentFromStats; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.removeLeadingDotSegment = exports.escape = exports.makeAbsolute = exports.unixify = void 0; const path = __webpack_require__(4); const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; @@ -14638,6 +14660,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(171); const micromatch = __webpack_require__(174); @@ -14654,6 +14677,14 @@ function isStaticPattern(pattern, options = {}) { } exports.isStaticPattern = isStaticPattern; function isDynamicPattern(pattern, options = {}) { + /** + * A special case with an empty string is necessary for matching patterns that start with a forward slash. + * An empty string cannot be a dynamic pattern. + * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. + */ + if (pattern === '') { + return false; + } /** * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check * filepath directly (without read directory). @@ -14728,12 +14759,23 @@ function expandBraceExpansion(pattern) { } exports.expandBraceExpansion = expandBraceExpansion; function getPatternParts(pattern, options) { - const info = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); - // See micromatch/picomatch#58 for more details - if (info.parts.length === 0) { - return [pattern]; + let { parts } = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); + /** + * The scan method returns an empty array in some cases. + * See micromatch/picomatch#58 for more details. + */ + if (parts.length === 0) { + parts = [pattern]; + } + /** + * The scan method does not return an empty part for the pattern with a forward slash. + * This is another part of micromatch/picomatch#58. + */ + if (parts[0].startsWith('/')) { + parts[0] = parts[0].slice(1); + parts.unshift(''); } - return info.parts; + return parts; } exports.getPatternParts = getPatternParts; function makeRe(pattern, options) { @@ -18947,6 +18989,7 @@ module.exports = parse; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.merge = void 0; const merge2 = __webpack_require__(146); function merge(streams) { const mergedStream = merge2(streams); @@ -18970,6 +19013,7 @@ function propagateCloseEventToSources(streams) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmpty = exports.isString = void 0; function isString(input) { return typeof input === 'string'; } @@ -20298,8 +20342,7 @@ class DeepFilter { return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions); } _filter(basePath, entry, matcher, negativeRe) { - const depth = this._getEntryLevel(basePath, entry.path); - if (this._isSkippedByDeep(depth)) { + if (this._isSkippedByDeep(basePath, entry.path)) { return false; } if (this._isSkippedSymbolicLink(entry)) { @@ -20311,22 +20354,31 @@ class DeepFilter { } return this._isSkippedByNegativePatterns(filepath, negativeRe); } - _isSkippedByDeep(entryDepth) { - return entryDepth >= this._settings.deep; - } - _isSkippedSymbolicLink(entry) { - return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); + _isSkippedByDeep(basePath, entryPath) { + /** + * Avoid unnecessary depth calculations when it doesn't matter. + */ + if (this._settings.deep === Infinity) { + return false; + } + return this._getEntryLevel(basePath, entryPath) >= this._settings.deep; } _getEntryLevel(basePath, entryPath) { - const basePathDepth = basePath.split('/').length; const entryPathDepth = entryPath.split('/').length; - return entryPathDepth - (basePath === '' ? 0 : basePathDepth); + if (basePath === '') { + return entryPathDepth; + } + const basePathDepth = basePath.split('/').length; + return entryPathDepth - basePathDepth; + } + _isSkippedSymbolicLink(entry) { + return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); } _isSkippedByPositivePatterns(entryPath, matcher) { return !this._settings.baseNameMatch && !matcher.match(entryPath); } - _isSkippedByNegativePatterns(entryPath, negativeRe) { - return !utils.pattern.matchAny(entryPath, negativeRe); + _isSkippedByNegativePatterns(entryPath, patternsRe) { + return !utils.pattern.matchAny(entryPath, patternsRe); } } exports.default = DeepFilter; @@ -20454,20 +20506,21 @@ class EntryFilter { return (entry) => this._filter(entry, positiveRe, negativeRe); } _filter(entry, positiveRe, negativeRe) { - if (this._settings.unique) { - if (this._isDuplicateEntry(entry)) { - return false; - } - this._createIndexRecord(entry); + if (this._settings.unique && this._isDuplicateEntry(entry)) { + return false; } if (this._onlyFileFilter(entry) || this._onlyDirectoryFilter(entry)) { return false; } - if (this._isSkippedByAbsoluteNegativePatterns(entry, negativeRe)) { + if (this._isSkippedByAbsoluteNegativePatterns(entry.path, negativeRe)) { return false; } const filepath = this._settings.baseNameMatch ? entry.name : entry.path; - return this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + const isMatched = this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + if (this._settings.unique && isMatched) { + this._createIndexRecord(entry); + } + return isMatched; } _isDuplicateEntry(entry) { return this.index.has(entry.path); @@ -20481,12 +20534,12 @@ class EntryFilter { _onlyDirectoryFilter(entry) { return this._settings.onlyDirectories && !entry.dirent.isDirectory(); } - _isSkippedByAbsoluteNegativePatterns(entry, negativeRe) { + _isSkippedByAbsoluteNegativePatterns(entryPath, patternsRe) { if (!this._settings.absolute) { return false; } - const fullpath = utils.path.makeAbsolute(this._settings.cwd, entry.path); - return this._isMatchToPatterns(fullpath, negativeRe); + const fullpath = utils.path.makeAbsolute(this._settings.cwd, entryPath); + return utils.pattern.matchAny(fullpath, patternsRe); } _isMatchToPatterns(entryPath, patternsRe) { const filepath = utils.path.removeLeadingDotSegment(entryPath); @@ -20676,9 +20729,14 @@ exports.default = ReaderSync; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0; const fs = __webpack_require__(134); const os = __webpack_require__(121); -const CPU_COUNT = os.cpus().length; +/** + * The `os.cpus` method can return zero. We expect the number of cores to be greater than zero. + * https://github.com/nodejs/node/blob/7faeddf23a98c53896f8b574a6e66589e8fb1eb8/lib/os.js#L106-L107 + */ +const CPU_COUNT = Math.max(os.cpus().length, 1); exports.DEFAULT_FILE_SYSTEM_ADAPTER = { lstat: fs.lstat, lstatSync: fs.lstatSync, @@ -22981,11 +23039,11 @@ class Project { ensureValidProjectDependency(project) { const relativePathToProject = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, project.path)); - const relativePathToProjectIfBazelPkg = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, `bazel/bin/packages/${path__WEBPACK_IMPORTED_MODULE_1___default.a.basename(project.path)}`)); + const relativePathToProjectIfBazelPkg = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, `${__dirname}/../../../bazel-bin/packages/${path__WEBPACK_IMPORTED_MODULE_1___default.a.basename(project.path)}/npm_module`)); const versionInPackageJson = this.allDependencies[project.name]; const expectedVersionInPackageJson = `link:${relativePathToProject}`; const expectedVersionInPackageJsonIfBazelPkg = `link:${relativePathToProjectIfBazelPkg}`; // TODO: after introduce bazel to build all the packages and completely remove the support for kbn packages - // do not allow child projects to hold dependencies + // do not allow child projects to hold dependencies, unless they are meant to be published externally if (versionInPackageJson === expectedVersionInPackageJson || versionInPackageJson === expectedVersionInPackageJsonIfBazelPkg) { return; @@ -23143,7 +23201,7 @@ const createProductionPackageJson = pkgJson => _objectSpread(_objectSpread({}, p dependencies: transformDependencies(pkgJson.dependencies) }); const isLinkDependency = depVersion => depVersion.startsWith('link:'); -const isBazelPackageDependency = depVersion => depVersion.startsWith('link:bazel/bin/'); +const isBazelPackageDependency = depVersion => depVersion.startsWith('link:bazel-bin/'); /** * Replaces `link:` dependencies with `file:` dependencies. When installing * dependencies, these `file:` dependencies will be copied into `node_modules` @@ -23153,7 +23211,7 @@ const isBazelPackageDependency = depVersion => depVersion.startsWith('link:bazel * will then _copy_ the `file:` dependencies into `node_modules` instead of * symlinking like we do in development. * - * Additionally it also taken care of replacing `link:bazel/bin/` with + * Additionally it also taken care of replacing `link:bazel-bin/` with * `file:` so we can also support the copy of the Bazel packages dist already into * build/packages to be copied into the node_modules */ @@ -23170,7 +23228,7 @@ function transformDependencies(dependencies = {}) { } if (isBazelPackageDependency(depVersion)) { - newDeps[name] = depVersion.replace('link:bazel/bin/', 'file:'); + newDeps[name] = depVersion.replace('link:bazel-bin/', 'file:').replace('/npm_module', ''); continue; } @@ -48065,8 +48123,10 @@ function addProjectToTree(tree, pathParts, project) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _ensure_yarn_integrity_exists__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(373); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ensureYarnIntegrityFileExists", function() { return _ensure_yarn_integrity_exists__WEBPACK_IMPORTED_MODULE_0__["ensureYarnIntegrityFileExists"]; }); +/* harmony import */ var _yarn_integrity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(373); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "yarnIntegrityFileExists", function() { return _yarn_integrity__WEBPACK_IMPORTED_MODULE_0__["yarnIntegrityFileExists"]; }); + +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ensureYarnIntegrityFileExists", function() { return _yarn_integrity__WEBPACK_IMPORTED_MODULE_0__["ensureYarnIntegrityFileExists"]; }); /* harmony import */ var _get_cache_folders__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(374); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getBazelDiskCacheFolder", function() { return _get_cache_folders__WEBPACK_IMPORTED_MODULE_1__["getBazelDiskCacheFolder"]; }); @@ -48099,6 +48159,7 @@ __webpack_require__.r(__webpack_exports__); "use strict"; __webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "yarnIntegrityFileExists", function() { return yarnIntegrityFileExists; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ensureYarnIntegrityFileExists", function() { return ensureYarnIntegrityFileExists; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); @@ -48112,9 +48173,27 @@ __webpack_require__.r(__webpack_exports__); */ +async function yarnIntegrityFileExists(nodeModulesPath) { + try { + const nodeModulesRealPath = await Object(_fs__WEBPACK_IMPORTED_MODULE_1__["tryRealpath"])(nodeModulesPath); + const yarnIntegrityFilePath = Object(path__WEBPACK_IMPORTED_MODULE_0__["join"])(nodeModulesRealPath, '.yarn-integrity'); // check if the file already exists + + if (await Object(_fs__WEBPACK_IMPORTED_MODULE_1__["isFile"])(yarnIntegrityFilePath)) { + return true; + } + } catch {// no-op + } + + return false; +} async function ensureYarnIntegrityFileExists(nodeModulesPath) { try { - await Object(_fs__WEBPACK_IMPORTED_MODULE_1__["writeFile"])(Object(path__WEBPACK_IMPORTED_MODULE_0__["join"])(nodeModulesPath, '.yarn-integrity'), '', { + const nodeModulesRealPath = await Object(_fs__WEBPACK_IMPORTED_MODULE_1__["tryRealpath"])(nodeModulesPath); + const yarnIntegrityFilePath = Object(path__WEBPACK_IMPORTED_MODULE_0__["join"])(nodeModulesRealPath, '.yarn-integrity'); // ensure node_modules folder is created + + await Object(_fs__WEBPACK_IMPORTED_MODULE_1__["mkdirp"])(nodeModulesRealPath); // write a blank file in case it doesn't exists + + await Object(_fs__WEBPACK_IMPORTED_MODULE_1__["writeFile"])(yarnIntegrityFilePath, '', { flag: 'wx' }); } catch {// no-op @@ -54377,7 +54456,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(7); tslib_1.__exportStar(__webpack_require__(476), exports); tslib_1.__exportStar(__webpack_require__(477), exports); - +//# sourceMappingURL=index.js.map /***/ }), /* 476 */ @@ -54436,7 +54515,7 @@ function observeLines(readable) { operators_1.catchError(() => Rx.empty()))); } exports.observeLines = observeLines; - +//# sourceMappingURL=observe_lines.js.map /***/ }), /* 477 */ @@ -54465,7 +54544,7 @@ function observeReadable(readable) { return Rx.race(Rx.fromEvent(readable, 'end').pipe(operators_1.first(), operators_1.ignoreElements()), Rx.fromEvent(readable, 'error').pipe(operators_1.first(), operators_1.mergeMap((err) => Rx.throwError(err)))); } exports.observeReadable = observeReadable; - +//# sourceMappingURL=observe_readable.js.map /***/ }), /* 478 */ @@ -59798,7 +59877,7 @@ class CiStatsReporter { } } exports.CiStatsReporter = CiStatsReporter; - +//# sourceMappingURL=ci_stats_reporter.js.map /***/ }), /* 516 */ @@ -63258,7 +63337,7 @@ function parseConfig(log) { return; } exports.parseConfig = parseConfig; - +//# sourceMappingURL=ci_stats_config.js.map /***/ }), /* 557 */ @@ -63599,7 +63678,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(783); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(812); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63625,7 +63704,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(783); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(812); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); @@ -63656,7 +63735,7 @@ async function buildBazelProductionProjects({ const projectNames = [...projects.values()].map(project => project.name); _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_4__["runBazel"])(['build', '//packages:build']); - _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete`); for (const project of projects.values()) { await copyToBuild(project, kibanaRoot, buildRoot); @@ -63680,7 +63759,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const relativeProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["relative"])(kibanaRoot, project.path); const buildProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, relativeProjectPath); await cpy__WEBPACK_IMPORTED_MODULE_0___default()(['**/*'], buildProjectPath, { - cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel', 'bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), + cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel-bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), dot: true, onlyFiles: true, parents: true @@ -63702,12 +63781,12 @@ async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { const buildProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, relativeProjectPath); const allPluginPaths = await globby__WEBPACK_IMPORTED_MODULE_1___default()([`**/*`], { onlyFiles: false, - cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel', 'bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), + cwd: buildProjectPath, dot: true }); for (const pluginPath of allPluginPaths) { - const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, pluginPath); + const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildProjectPath, pluginPath); if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(resolvedPluginPath)) { await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o644); @@ -90227,265 +90306,385 @@ module.exports = CpyError; "use strict"; -const arrayUnion = __webpack_require__(775); -const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(572); -const dirGlob = __webpack_require__(776); -const gitignore = __webpack_require__(780); +const fs = __webpack_require__(134); +const arrayUnion = __webpack_require__(145); +const merge2 = __webpack_require__(146); +const fastGlob = __webpack_require__(775); +const dirGlob = __webpack_require__(232); +const gitignore = __webpack_require__(810); +const {FilterStream, UniqueStream} = __webpack_require__(811); const DEFAULT_FILTER = () => false; const isNegative = pattern => pattern[0] === '!'; const assertPatternsInput = patterns => { - if (!patterns.every(x => typeof x === 'string')) { + if (!patterns.every(pattern => typeof pattern === 'string')) { throw new TypeError('Patterns must be a string or an array of strings'); } }; -const generateGlobTasks = (patterns, taskOpts) => { - patterns = [].concat(patterns); +const checkCwdOption = (options = {}) => { + if (!options.cwd) { + return; + } + + let stat; + try { + stat = fs.statSync(options.cwd); + } catch { + return; + } + + if (!stat.isDirectory()) { + throw new Error('The `cwd` option must be a path to a directory'); + } +}; + +const getPathString = p => p.stats instanceof fs.Stats ? p.path : p; + +const generateGlobTasks = (patterns, taskOptions) => { + patterns = arrayUnion([].concat(patterns)); assertPatternsInput(patterns); + checkCwdOption(taskOptions); const globTasks = []; - taskOpts = Object.assign({ + taskOptions = { ignore: [], - expandDirectories: true - }, taskOpts); + expandDirectories: true, + ...taskOptions + }; - patterns.forEach((pattern, i) => { + for (const [index, pattern] of patterns.entries()) { if (isNegative(pattern)) { - return; + continue; } const ignore = patterns - .slice(i) - .filter(isNegative) + .slice(index) + .filter(pattern => isNegative(pattern)) .map(pattern => pattern.slice(1)); - const opts = Object.assign({}, taskOpts, { - ignore: taskOpts.ignore.concat(ignore) - }); + const options = { + ...taskOptions, + ignore: taskOptions.ignore.concat(ignore) + }; - globTasks.push({pattern, opts}); - }); + globTasks.push({pattern, options}); + } return globTasks; }; const globDirs = (task, fn) => { - let opts = {cwd: task.opts.cwd}; + let options = {}; + if (task.options.cwd) { + options.cwd = task.options.cwd; + } - if (Array.isArray(task.opts.expandDirectories)) { - opts = Object.assign(opts, {files: task.opts.expandDirectories}); - } else if (typeof task.opts.expandDirectories === 'object') { - opts = Object.assign(opts, task.opts.expandDirectories); + if (Array.isArray(task.options.expandDirectories)) { + options = { + ...options, + files: task.options.expandDirectories + }; + } else if (typeof task.options.expandDirectories === 'object') { + options = { + ...options, + ...task.options.expandDirectories + }; } - return fn(task.pattern, opts); + return fn(task.pattern, options); }; -const getPattern = (task, fn) => task.opts.expandDirectories ? globDirs(task, fn) : [task.pattern]; +const getPattern = (task, fn) => task.options.expandDirectories ? globDirs(task, fn) : [task.pattern]; -module.exports = (patterns, opts) => { - let globTasks; +const getFilterSync = options => { + return options && options.gitignore ? + gitignore.sync({cwd: options.cwd, ignore: options.ignore}) : + DEFAULT_FILTER; +}; - try { - globTasks = generateGlobTasks(patterns, opts); - } catch (err) { - return Promise.reject(err); +const globToTask = task => glob => { + const {options} = task; + if (options.ignore && Array.isArray(options.ignore) && options.expandDirectories) { + options.ignore = dirGlob.sync(options.ignore); } - const getTasks = Promise.all(globTasks.map(task => Promise.resolve(getPattern(task, dirGlob)) - .then(globs => Promise.all(globs.map(glob => ({ - pattern: glob, - opts: task.opts - })))) - )) - .then(tasks => arrayUnion.apply(null, tasks)); - - const getFilter = () => { - return Promise.resolve( - opts && opts.gitignore ? - gitignore({cwd: opts.cwd, ignore: opts.ignore}) : - DEFAULT_FILTER - ); + return { + pattern: glob, + options }; - - return getFilter() - .then(filter => { - return getTasks - .then(tasks => Promise.all(tasks.map(task => fastGlob(task.pattern, task.opts)))) - .then(paths => arrayUnion.apply(null, paths)) - .then(paths => paths.filter(p => !filter(p))); - }); }; -module.exports.sync = (patterns, opts) => { - const globTasks = generateGlobTasks(patterns, opts); +module.exports = async (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); - const getFilter = () => { - return opts && opts.gitignore ? - gitignore.sync({cwd: opts.cwd, ignore: opts.ignore}) : + const getFilter = async () => { + return options && options.gitignore ? + gitignore({cwd: options.cwd, ignore: options.ignore}) : DEFAULT_FILTER; }; - const tasks = globTasks.reduce((tasks, task) => { - const newTask = getPattern(task, dirGlob.sync).map(glob => ({ - pattern: glob, - opts: task.opts + const getTasks = async () => { + const tasks = await Promise.all(globTasks.map(async task => { + const globs = await getPattern(task, dirGlob); + return Promise.all(globs.map(globToTask(task))); })); - return tasks.concat(newTask); - }, []); - - const filter = getFilter(); - - return tasks.reduce( - (matches, task) => arrayUnion(matches, fastGlob.sync(task.pattern, task.opts)), - [] - ).filter(p => !filter(p)); -}; - -module.exports.generateGlobTasks = generateGlobTasks; - -module.exports.hasMagic = (patterns, opts) => [] - .concat(patterns) - .some(pattern => glob.hasMagic(pattern, opts)); - -module.exports.gitignore = gitignore; + return arrayUnion(...tasks); + }; -/***/ }), -/* 775 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -var arrayUniq = __webpack_require__(571); + const [filter, tasks] = await Promise.all([getFilter(), getTasks()]); + const paths = await Promise.all(tasks.map(task => fastGlob(task.pattern, task.options))); -module.exports = function () { - return arrayUniq([].concat.apply([], arguments)); + return arrayUnion(...paths).filter(path_ => !filter(getPathString(path_))); }; +module.exports.sync = (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); -/***/ }), -/* 776 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -const path = __webpack_require__(4); -const arrify = __webpack_require__(777); -const pathType = __webpack_require__(778); + const tasks = []; + for (const task of globTasks) { + const newTask = getPattern(task, dirGlob.sync).map(globToTask(task)); + tasks.push(...newTask); + } -const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; -const getPath = filepath => filepath[0] === '!' ? filepath.slice(1) : filepath; + const filter = getFilterSync(options); -const addExtensions = (file, extensions) => { - if (path.extname(file)) { - return `**/${file}`; + let matches = []; + for (const task of tasks) { + matches = arrayUnion(matches, fastGlob.sync(task.pattern, task.options)); } - return `**/${file}.${getExtensions(extensions)}`; + return matches.filter(path_ => !filter(path_)); }; -const getGlob = (dir, opts) => { - opts = Object.assign({}, opts); - - if (opts.files && !Array.isArray(opts.files)) { - throw new TypeError(`\`options.files\` must be an \`Array\`, not \`${typeof opts.files}\``); - } +module.exports.stream = (patterns, options) => { + const globTasks = generateGlobTasks(patterns, options); - if (opts.extensions && !Array.isArray(opts.extensions)) { - throw new TypeError(`\`options.extensions\` must be an \`Array\`, not \`${typeof opts.extensions}\``); + const tasks = []; + for (const task of globTasks) { + const newTask = getPattern(task, dirGlob.sync).map(globToTask(task)); + tasks.push(...newTask); } - if (opts.files && opts.extensions) { - return opts.files.map(x => path.join(dir, addExtensions(x, opts.extensions))); - } else if (opts.files) { - return opts.files.map(x => path.join(dir, `**/${x}`)); - } else if (opts.extensions) { - return [path.join(dir, `**/*.${getExtensions(opts.extensions)}`)]; - } + const filter = getFilterSync(options); + const filterStream = new FilterStream(p => !filter(p)); + const uniqueStream = new UniqueStream(); - return [path.join(dir, '**')]; + return merge2(tasks.map(task => fastGlob.stream(task.pattern, task.options))) + .pipe(filterStream) + .pipe(uniqueStream); }; -module.exports = (input, opts) => { - return Promise.all(arrify(input).map(x => pathType.dir(getPath(x)) - .then(isDir => isDir ? getGlob(x, opts) : x))) - .then(globs => [].concat.apply([], globs)); -}; +module.exports.generateGlobTasks = generateGlobTasks; -module.exports.sync = (input, opts) => { - const globs = arrify(input).map(x => pathType.dirSync(getPath(x)) ? getGlob(x, opts) : x); - return [].concat.apply([], globs); -}; +module.exports.hasMagic = (patterns, options) => [] + .concat(patterns) + .some(pattern => fastGlob.isDynamicPattern(pattern, options)); + +module.exports.gitignore = gitignore; /***/ }), -/* 777 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; - -module.exports = function (val) { - if (val === null || val === undefined) { - return []; - } - - return Array.isArray(val) ? val : [val]; -}; + +const taskManager = __webpack_require__(776); +const async_1 = __webpack_require__(796); +const stream_1 = __webpack_require__(806); +const sync_1 = __webpack_require__(807); +const settings_1 = __webpack_require__(809); +const utils = __webpack_require__(777); +async function FastGlob(source, options) { + assertPatternsInput(source); + const works = getWorks(source, async_1.default, options); + const result = await Promise.all(works); + return utils.array.flatten(result); +} +// https://github.com/typescript-eslint/typescript-eslint/issues/60 +// eslint-disable-next-line no-redeclare +(function (FastGlob) { + function sync(source, options) { + assertPatternsInput(source); + const works = getWorks(source, sync_1.default, options); + return utils.array.flatten(works); + } + FastGlob.sync = sync; + function stream(source, options) { + assertPatternsInput(source); + const works = getWorks(source, stream_1.default, options); + /** + * The stream returned by the provider cannot work with an asynchronous iterator. + * To support asynchronous iterators, regardless of the number of tasks, we always multiplex streams. + * This affects performance (+25%). I don't see best solution right now. + */ + return utils.stream.merge(works); + } + FastGlob.stream = stream; + function generateTasks(source, options) { + assertPatternsInput(source); + const patterns = [].concat(source); + const settings = new settings_1.default(options); + return taskManager.generate(patterns, settings); + } + FastGlob.generateTasks = generateTasks; + function isDynamicPattern(source, options) { + assertPatternsInput(source); + const settings = new settings_1.default(options); + return utils.pattern.isDynamicPattern(source, settings); + } + FastGlob.isDynamicPattern = isDynamicPattern; + function escapePath(source) { + assertPatternsInput(source); + return utils.path.escape(source); + } + FastGlob.escapePath = escapePath; +})(FastGlob || (FastGlob = {})); +function getWorks(source, _Provider, options) { + const patterns = [].concat(source); + const settings = new settings_1.default(options); + const tasks = taskManager.generate(patterns, settings); + const provider = new _Provider(settings); + return tasks.map(provider.read, provider); +} +function assertPatternsInput(input) { + const source = [].concat(input); + const isValidSource = source.every((item) => utils.string.isString(item) && !utils.string.isEmpty(item)); + if (!isValidSource) { + throw new TypeError('Patterns must be a string (non empty) or an array of strings'); + } +} +module.exports = FastGlob; /***/ }), -/* 778 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; +const utils = __webpack_require__(777); +function generate(patterns, settings) { + const positivePatterns = getPositivePatterns(patterns); + const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); + const staticPatterns = positivePatterns.filter((pattern) => utils.pattern.isStaticPattern(pattern, settings)); + const dynamicPatterns = positivePatterns.filter((pattern) => utils.pattern.isDynamicPattern(pattern, settings)); + const staticTasks = convertPatternsToTasks(staticPatterns, negativePatterns, /* dynamic */ false); + const dynamicTasks = convertPatternsToTasks(dynamicPatterns, negativePatterns, /* dynamic */ true); + return staticTasks.concat(dynamicTasks); +} +exports.generate = generate; +function convertPatternsToTasks(positive, negative, dynamic) { + const positivePatternsGroup = groupPatternsByBaseDirectory(positive); + // When we have a global group – there is no reason to divide the patterns into independent tasks. + // In this case, the global task covers the rest. + if ('.' in positivePatternsGroup) { + const task = convertPatternGroupToTask('.', positive, negative, dynamic); + return [task]; + } + return convertPatternGroupsToTasks(positivePatternsGroup, negative, dynamic); +} +exports.convertPatternsToTasks = convertPatternsToTasks; +function getPositivePatterns(patterns) { + return utils.pattern.getPositivePatterns(patterns); +} +exports.getPositivePatterns = getPositivePatterns; +function getNegativePatternsAsPositive(patterns, ignore) { + const negative = utils.pattern.getNegativePatterns(patterns).concat(ignore); + const positive = negative.map(utils.pattern.convertToPositivePattern); + return positive; +} +exports.getNegativePatternsAsPositive = getNegativePatternsAsPositive; +function groupPatternsByBaseDirectory(patterns) { + const group = {}; + return patterns.reduce((collection, pattern) => { + const base = utils.pattern.getBaseDirectory(pattern); + if (base in collection) { + collection[base].push(pattern); + } + else { + collection[base] = [pattern]; + } + return collection; + }, group); +} +exports.groupPatternsByBaseDirectory = groupPatternsByBaseDirectory; +function convertPatternGroupsToTasks(positive, negative, dynamic) { + return Object.keys(positive).map((base) => { + return convertPatternGroupToTask(base, positive[base], negative, dynamic); + }); +} +exports.convertPatternGroupsToTasks = convertPatternGroupsToTasks; +function convertPatternGroupToTask(base, positive, negative, dynamic) { + return { + dynamic, + positive, + negative, + base, + patterns: [].concat(positive, negative.map(utils.pattern.convertToNegativePattern)) + }; +} +exports.convertPatternGroupToTask = convertPatternGroupToTask; -const fs = __webpack_require__(134); -const pify = __webpack_require__(779); - -function type(fn, fn2, fp) { - if (typeof fp !== 'string') { - return Promise.reject(new TypeError(`Expected a string, got ${typeof fp}`)); - } - - return pify(fs[fn])(fp) - .then(stats => stats[fn2]()) - .catch(err => { - if (err.code === 'ENOENT') { - return false; - } - throw err; - }); -} +/***/ }), +/* 777 */ +/***/ (function(module, exports, __webpack_require__) { -function typeSync(fn, fn2, fp) { - if (typeof fp !== 'string') { - throw new TypeError(`Expected a string, got ${typeof fp}`); - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; +const array = __webpack_require__(778); +exports.array = array; +const errno = __webpack_require__(779); +exports.errno = errno; +const fs = __webpack_require__(780); +exports.fs = fs; +const path = __webpack_require__(781); +exports.path = path; +const pattern = __webpack_require__(782); +exports.pattern = pattern; +const stream = __webpack_require__(794); +exports.stream = stream; +const string = __webpack_require__(795); +exports.string = string; - try { - return fs[fn](fp)[fn2](); - } catch (err) { - if (err.code === 'ENOENT') { - return false; - } - throw err; - } -} +/***/ }), +/* 778 */ +/***/ (function(module, exports, __webpack_require__) { -exports.file = type.bind(null, 'stat', 'isFile'); -exports.dir = type.bind(null, 'stat', 'isDirectory'); -exports.symlink = type.bind(null, 'lstat', 'isSymbolicLink'); -exports.fileSync = typeSync.bind(null, 'statSync', 'isFile'); -exports.dirSync = typeSync.bind(null, 'statSync', 'isDirectory'); -exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.splitWhen = exports.flatten = void 0; +function flatten(items) { + return items.reduce((collection, item) => [].concat(collection, item), []); +} +exports.flatten = flatten; +function splitWhen(items, predicate) { + const result = [[]]; + let groupIndex = 0; + for (const item of items) { + if (predicate(item)) { + groupIndex++; + result[groupIndex] = []; + } + else { + result[groupIndex].push(item); + } + } + return result; +} +exports.splitWhen = splitWhen; /***/ }), @@ -90493,155 +90692,2976 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ (function(module, exports, __webpack_require__) { "use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEnoentCodeError = void 0; +function isEnoentCodeError(error) { + return error.code === 'ENOENT'; +} +exports.isEnoentCodeError = isEnoentCodeError; -const processFn = (fn, opts) => function () { - const P = opts.promiseModule; - const args = new Array(arguments.length); +/***/ }), +/* 780 */ +/***/ (function(module, exports, __webpack_require__) { - for (let i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDirentFromStats = void 0; +class DirentFromStats { + constructor(name, stats) { + this.name = name; + this.isBlockDevice = stats.isBlockDevice.bind(stats); + this.isCharacterDevice = stats.isCharacterDevice.bind(stats); + this.isDirectory = stats.isDirectory.bind(stats); + this.isFIFO = stats.isFIFO.bind(stats); + this.isFile = stats.isFile.bind(stats); + this.isSocket = stats.isSocket.bind(stats); + this.isSymbolicLink = stats.isSymbolicLink.bind(stats); + } +} +function createDirentFromStats(name, stats) { + return new DirentFromStats(name, stats); +} +exports.createDirentFromStats = createDirentFromStats; - return new P((resolve, reject) => { - if (opts.errorFirst) { - args.push(function (err, result) { - if (opts.multiArgs) { - const results = new Array(arguments.length - 1); - for (let i = 1; i < arguments.length; i++) { - results[i - 1] = arguments[i]; - } +/***/ }), +/* 781 */ +/***/ (function(module, exports, __webpack_require__) { - if (err) { - results.unshift(err); - reject(results); - } else { - resolve(results); - } - } else if (err) { - reject(err); - } else { - resolve(result); - } - }); - } else { - args.push(function (result) { - if (opts.multiArgs) { - const results = new Array(arguments.length - 1); +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.removeLeadingDotSegment = exports.escape = exports.makeAbsolute = exports.unixify = void 0; +const path = __webpack_require__(4); +const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\ +const UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g; +/** + * Designed to work only with simple paths: `dir\\file`. + */ +function unixify(filepath) { + return filepath.replace(/\\/g, '/'); +} +exports.unixify = unixify; +function makeAbsolute(cwd, filepath) { + return path.resolve(cwd, filepath); +} +exports.makeAbsolute = makeAbsolute; +function escape(pattern) { + return pattern.replace(UNESCAPED_GLOB_SYMBOLS_RE, '\\$2'); +} +exports.escape = escape; +function removeLeadingDotSegment(entry) { + // We do not use `startsWith` because this is 10x slower than current implementation for some cases. + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + if (entry.charAt(0) === '.') { + const secondCharactery = entry.charAt(1); + if (secondCharactery === '/' || secondCharactery === '\\') { + return entry.slice(LEADING_DOT_SEGMENT_CHARACTERS_COUNT); + } + } + return entry; +} +exports.removeLeadingDotSegment = removeLeadingDotSegment; + + +/***/ }), +/* 782 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; +const path = __webpack_require__(4); +const globParent = __webpack_require__(171); +const micromatch = __webpack_require__(783); +const picomatch = __webpack_require__(185); +const GLOBSTAR = '**'; +const ESCAPE_SYMBOL = '\\'; +const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; +const REGEX_CHARACTER_CLASS_SYMBOLS_RE = /\[.*]/; +const REGEX_GROUP_SYMBOLS_RE = /(?:^|[^!*+?@])\(.*\|.*\)/; +const GLOB_EXTENSION_SYMBOLS_RE = /[!*+?@]\(.*\)/; +const BRACE_EXPANSIONS_SYMBOLS_RE = /{.*(?:,|\.\.).*}/; +function isStaticPattern(pattern, options = {}) { + return !isDynamicPattern(pattern, options); +} +exports.isStaticPattern = isStaticPattern; +function isDynamicPattern(pattern, options = {}) { + /** + * A special case with an empty string is necessary for matching patterns that start with a forward slash. + * An empty string cannot be a dynamic pattern. + * For example, the pattern `/lib/*` will be spread into parts: '', 'lib', '*'. + */ + if (pattern === '') { + return false; + } + /** + * When the `caseSensitiveMatch` option is disabled, all patterns must be marked as dynamic, because we cannot check + * filepath directly (without read directory). + */ + if (options.caseSensitiveMatch === false || pattern.includes(ESCAPE_SYMBOL)) { + return true; + } + if (COMMON_GLOB_SYMBOLS_RE.test(pattern) || REGEX_CHARACTER_CLASS_SYMBOLS_RE.test(pattern) || REGEX_GROUP_SYMBOLS_RE.test(pattern)) { + return true; + } + if (options.extglob !== false && GLOB_EXTENSION_SYMBOLS_RE.test(pattern)) { + return true; + } + if (options.braceExpansion !== false && BRACE_EXPANSIONS_SYMBOLS_RE.test(pattern)) { + return true; + } + return false; +} +exports.isDynamicPattern = isDynamicPattern; +function convertToPositivePattern(pattern) { + return isNegativePattern(pattern) ? pattern.slice(1) : pattern; +} +exports.convertToPositivePattern = convertToPositivePattern; +function convertToNegativePattern(pattern) { + return '!' + pattern; +} +exports.convertToNegativePattern = convertToNegativePattern; +function isNegativePattern(pattern) { + return pattern.startsWith('!') && pattern[1] !== '('; +} +exports.isNegativePattern = isNegativePattern; +function isPositivePattern(pattern) { + return !isNegativePattern(pattern); +} +exports.isPositivePattern = isPositivePattern; +function getNegativePatterns(patterns) { + return patterns.filter(isNegativePattern); +} +exports.getNegativePatterns = getNegativePatterns; +function getPositivePatterns(patterns) { + return patterns.filter(isPositivePattern); +} +exports.getPositivePatterns = getPositivePatterns; +function getBaseDirectory(pattern) { + return globParent(pattern, { flipBackslashes: false }); +} +exports.getBaseDirectory = getBaseDirectory; +function hasGlobStar(pattern) { + return pattern.includes(GLOBSTAR); +} +exports.hasGlobStar = hasGlobStar; +function endsWithSlashGlobStar(pattern) { + return pattern.endsWith('/' + GLOBSTAR); +} +exports.endsWithSlashGlobStar = endsWithSlashGlobStar; +function isAffectDepthOfReadingPattern(pattern) { + const basename = path.basename(pattern); + return endsWithSlashGlobStar(pattern) || isStaticPattern(basename); +} +exports.isAffectDepthOfReadingPattern = isAffectDepthOfReadingPattern; +function expandPatternsWithBraceExpansion(patterns) { + return patterns.reduce((collection, pattern) => { + return collection.concat(expandBraceExpansion(pattern)); + }, []); +} +exports.expandPatternsWithBraceExpansion = expandPatternsWithBraceExpansion; +function expandBraceExpansion(pattern) { + return micromatch.braces(pattern, { + expand: true, + nodupes: true + }); +} +exports.expandBraceExpansion = expandBraceExpansion; +function getPatternParts(pattern, options) { + let { parts } = picomatch.scan(pattern, Object.assign(Object.assign({}, options), { parts: true })); + /** + * The scan method returns an empty array in some cases. + * See micromatch/picomatch#58 for more details. + */ + if (parts.length === 0) { + parts = [pattern]; + } + /** + * The scan method does not return an empty part for the pattern with a forward slash. + * This is another part of micromatch/picomatch#58. + */ + if (parts[0].startsWith('/')) { + parts[0] = parts[0].slice(1); + parts.unshift(''); + } + return parts; +} +exports.getPatternParts = getPatternParts; +function makeRe(pattern, options) { + return micromatch.makeRe(pattern, options); +} +exports.makeRe = makeRe; +function convertPatternsToRe(patterns, options) { + return patterns.map((pattern) => makeRe(pattern, options)); +} +exports.convertPatternsToRe = convertPatternsToRe; +function matchAny(entry, patternsRe) { + return patternsRe.some((patternRe) => patternRe.test(entry)); +} +exports.matchAny = matchAny; + + +/***/ }), +/* 783 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const util = __webpack_require__(112); +const braces = __webpack_require__(784); +const picomatch = __webpack_require__(185); +const utils = __webpack_require__(188); +const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); + +/** + * Returns an array of strings that match one or more glob patterns. + * + * ```js + * const mm = require('micromatch'); + * // mm(list, patterns[, options]); + * + * console.log(mm(['a.js', 'a.txt'], ['*.js'])); + * //=> [ 'a.js' ] + * ``` + * @param {String|Array} list List of strings to match. + * @param {String|Array} patterns One or more glob patterns to use for matching. + * @param {Object} options See available [options](#options) + * @return {Array} Returns an array of matches + * @summary false + * @api public + */ + +const micromatch = (list, patterns, options) => { + patterns = [].concat(patterns); + list = [].concat(list); + + let omit = new Set(); + let keep = new Set(); + let items = new Set(); + let negatives = 0; + + let onResult = state => { + items.add(state.output); + if (options && options.onResult) { + options.onResult(state); + } + }; + + for (let i = 0; i < patterns.length; i++) { + let isMatch = picomatch(String(patterns[i]), { ...options, onResult }, true); + let negated = isMatch.state.negated || isMatch.state.negatedExtglob; + if (negated) negatives++; + + for (let item of list) { + let matched = isMatch(item, true); + + let match = negated ? !matched.isMatch : matched.isMatch; + if (!match) continue; + + if (negated) { + omit.add(matched.output); + } else { + omit.delete(matched.output); + keep.add(matched.output); + } + } + } + + let result = negatives === patterns.length ? [...items] : [...keep]; + let matches = result.filter(item => !omit.has(item)); + + if (options && matches.length === 0) { + if (options.failglob === true) { + throw new Error(`No matches found for "${patterns.join(', ')}"`); + } + + if (options.nonull === true || options.nullglob === true) { + return options.unescape ? patterns.map(p => p.replace(/\\/g, '')) : patterns; + } + } + + return matches; +}; + +/** + * Backwards compatibility + */ + +micromatch.match = micromatch; + +/** + * Returns a matcher function from the given glob `pattern` and `options`. + * The returned function takes a string to match as its only argument and returns + * true if the string is a match. + * + * ```js + * const mm = require('micromatch'); + * // mm.matcher(pattern[, options]); + * + * const isMatch = mm.matcher('*.!(*a)'); + * console.log(isMatch('a.a')); //=> false + * console.log(isMatch('a.b')); //=> true + * ``` + * @param {String} `pattern` Glob pattern + * @param {Object} `options` + * @return {Function} Returns a matcher function. + * @api public + */ + +micromatch.matcher = (pattern, options) => picomatch(pattern, options); + +/** + * Returns true if **any** of the given glob `patterns` match the specified `string`. + * + * ```js + * const mm = require('micromatch'); + * // mm.isMatch(string, patterns[, options]); + * + * console.log(mm.isMatch('a.a', ['b.*', '*.a'])); //=> true + * console.log(mm.isMatch('a.a', 'b.*')); //=> false + * ``` + * @param {String} str The string to test. + * @param {String|Array} patterns One or more glob patterns to use for matching. + * @param {Object} [options] See available [options](#options). + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str); + +/** + * Backwards compatibility + */ + +micromatch.any = micromatch.isMatch; + +/** + * Returns a list of strings that _**do not match any**_ of the given `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.not(list, patterns[, options]); + * + * console.log(mm.not(['a.a', 'b.b', 'c.c'], '*.a')); + * //=> ['b.b', 'c.c'] + * ``` + * @param {Array} `list` Array of strings to match. + * @param {String|Array} `patterns` One or more glob pattern to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Array} Returns an array of strings that **do not match** the given patterns. + * @api public + */ + +micromatch.not = (list, patterns, options = {}) => { + patterns = [].concat(patterns).map(String); + let result = new Set(); + let items = []; + + let onResult = state => { + if (options.onResult) options.onResult(state); + items.push(state.output); + }; + + let matches = micromatch(list, patterns, { ...options, onResult }); + + for (let item of items) { + if (!matches.includes(item)) { + result.add(item); + } + } + return [...result]; +}; + +/** + * Returns true if the given `string` contains the given pattern. Similar + * to [.isMatch](#isMatch) but the pattern can match any part of the string. + * + * ```js + * var mm = require('micromatch'); + * // mm.contains(string, pattern[, options]); + * + * console.log(mm.contains('aa/bb/cc', '*b')); + * //=> true + * console.log(mm.contains('aa/bb/cc', '*d')); + * //=> false + * ``` + * @param {String} `str` The string to match. + * @param {String|Array} `patterns` Glob pattern to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if the patter matches any part of `str`. + * @api public + */ + +micromatch.contains = (str, pattern, options) => { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string: "${util.inspect(str)}"`); + } + + if (Array.isArray(pattern)) { + return pattern.some(p => micromatch.contains(str, p, options)); + } + + if (typeof pattern === 'string') { + if (isEmptyString(str) || isEmptyString(pattern)) { + return false; + } + + if (str.includes(pattern) || (str.startsWith('./') && str.slice(2).includes(pattern))) { + return true; + } + } + + return micromatch.isMatch(str, pattern, { ...options, contains: true }); +}; + +/** + * Filter the keys of the given object with the given `glob` pattern + * and `options`. Does not attempt to match nested keys. If you need this feature, + * use [glob-object][] instead. + * + * ```js + * const mm = require('micromatch'); + * // mm.matchKeys(object, patterns[, options]); + * + * const obj = { aa: 'a', ab: 'b', ac: 'c' }; + * console.log(mm.matchKeys(obj, '*b')); + * //=> { ab: 'b' } + * ``` + * @param {Object} `object` The object with keys to filter. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Object} Returns an object with only keys that match the given patterns. + * @api public + */ + +micromatch.matchKeys = (obj, patterns, options) => { + if (!utils.isObject(obj)) { + throw new TypeError('Expected the first argument to be an object'); + } + let keys = micromatch(Object.keys(obj), patterns, options); + let res = {}; + for (let key of keys) res[key] = obj[key]; + return res; +}; + +/** + * Returns true if some of the strings in the given `list` match any of the given glob `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.some(list, patterns[, options]); + * + * console.log(mm.some(['foo.js', 'bar.js'], ['*.js', '!foo.js'])); + * // true + * console.log(mm.some(['foo.js'], ['*.js', '!foo.js'])); + * // false + * ``` + * @param {String|Array} `list` The string or array of strings to test. Returns as soon as the first match is found. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.some = (list, patterns, options) => { + let items = [].concat(list); + + for (let pattern of [].concat(patterns)) { + let isMatch = picomatch(String(pattern), options); + if (items.some(item => isMatch(item))) { + return true; + } + } + return false; +}; + +/** + * Returns true if every string in the given `list` matches + * any of the given glob `patterns`. + * + * ```js + * const mm = require('micromatch'); + * // mm.every(list, patterns[, options]); + * + * console.log(mm.every('foo.js', ['foo.js'])); + * // true + * console.log(mm.every(['foo.js', 'bar.js'], ['*.js'])); + * // true + * console.log(mm.every(['foo.js', 'bar.js'], ['*.js', '!foo.js'])); + * // false + * console.log(mm.every(['foo.js'], ['*.js', '!foo.js'])); + * // false + * ``` + * @param {String|Array} `list` The string or array of strings to test. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.every = (list, patterns, options) => { + let items = [].concat(list); + + for (let pattern of [].concat(patterns)) { + let isMatch = picomatch(String(pattern), options); + if (!items.every(item => isMatch(item))) { + return false; + } + } + return true; +}; + +/** + * Returns true if **all** of the given `patterns` match + * the specified string. + * + * ```js + * const mm = require('micromatch'); + * // mm.all(string, patterns[, options]); + * + * console.log(mm.all('foo.js', ['foo.js'])); + * // true + * + * console.log(mm.all('foo.js', ['*.js', '!foo.js'])); + * // false + * + * console.log(mm.all('foo.js', ['*.js', 'foo.js'])); + * // true + * + * console.log(mm.all('foo.js', ['*.js', 'f*', '*o*', '*o.js'])); + * // true + * ``` + * @param {String|Array} `str` The string to test. + * @param {String|Array} `patterns` One or more glob patterns to use for matching. + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns true if any patterns match `str` + * @api public + */ + +micromatch.all = (str, patterns, options) => { + if (typeof str !== 'string') { + throw new TypeError(`Expected a string: "${util.inspect(str)}"`); + } + + return [].concat(patterns).every(p => picomatch(p, options)(str)); +}; + +/** + * Returns an array of matches captured by `pattern` in `string, or `null` if the pattern did not match. + * + * ```js + * const mm = require('micromatch'); + * // mm.capture(pattern, string[, options]); + * + * console.log(mm.capture('test/*.js', 'test/foo.js')); + * //=> ['foo'] + * console.log(mm.capture('test/*.js', 'foo/bar.css')); + * //=> null + * ``` + * @param {String} `glob` Glob pattern to use for matching. + * @param {String} `input` String to match + * @param {Object} `options` See available [options](#options) for changing how matches are performed + * @return {Boolean} Returns an array of captures if the input matches the glob pattern, otherwise `null`. + * @api public + */ + +micromatch.capture = (glob, input, options) => { + let posix = utils.isWindows(options); + let regex = picomatch.makeRe(String(glob), { ...options, capture: true }); + let match = regex.exec(posix ? utils.toPosixSlashes(input) : input); + + if (match) { + return match.slice(1).map(v => v === void 0 ? '' : v); + } +}; + +/** + * Create a regular expression from the given glob `pattern`. + * + * ```js + * const mm = require('micromatch'); + * // mm.makeRe(pattern[, options]); + * + * console.log(mm.makeRe('*.js')); + * //=> /^(?:(\.[\\\/])?(?!\.)(?=.)[^\/]*?\.js)$/ + * ``` + * @param {String} `pattern` A glob pattern to convert to regex. + * @param {Object} `options` + * @return {RegExp} Returns a regex created from the given pattern. + * @api public + */ + +micromatch.makeRe = (...args) => picomatch.makeRe(...args); + +/** + * Scan a glob pattern to separate the pattern into segments. Used + * by the [split](#split) method. + * + * ```js + * const mm = require('micromatch'); + * const state = mm.scan(pattern[, options]); + * ``` + * @param {String} `pattern` + * @param {Object} `options` + * @return {Object} Returns an object with + * @api public + */ + +micromatch.scan = (...args) => picomatch.scan(...args); + +/** + * Parse a glob pattern to create the source string for a regular + * expression. + * + * ```js + * const mm = require('micromatch'); + * const state = mm(pattern[, options]); + * ``` + * @param {String} `glob` + * @param {Object} `options` + * @return {Object} Returns an object with useful properties and output to be used as regex source string. + * @api public + */ + +micromatch.parse = (patterns, options) => { + let res = []; + for (let pattern of [].concat(patterns || [])) { + for (let str of braces(String(pattern), options)) { + res.push(picomatch.parse(str, options)); + } + } + return res; +}; + +/** + * Process the given brace `pattern`. + * + * ```js + * const { braces } = require('micromatch'); + * console.log(braces('foo/{a,b,c}/bar')); + * //=> [ 'foo/(a|b|c)/bar' ] + * + * console.log(braces('foo/{a,b,c}/bar', { expand: true })); + * //=> [ 'foo/a/bar', 'foo/b/bar', 'foo/c/bar' ] + * ``` + * @param {String} `pattern` String with brace pattern to process. + * @param {Object} `options` Any [options](#options) to change how expansion is performed. See the [braces][] library for all available options. + * @return {Array} + * @api public + */ + +micromatch.braces = (pattern, options) => { + if (typeof pattern !== 'string') throw new TypeError('Expected a string'); + if ((options && options.nobrace === true) || !/\{.*\}/.test(pattern)) { + return [pattern]; + } + return braces(pattern, options); +}; + +/** + * Expand braces + */ + +micromatch.braceExpand = (pattern, options) => { + if (typeof pattern !== 'string') throw new TypeError('Expected a string'); + return micromatch.braces(pattern, { ...options, expand: true }); +}; + +/** + * Expose micromatch + */ + +module.exports = micromatch; + + +/***/ }), +/* 784 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const stringify = __webpack_require__(785); +const compile = __webpack_require__(787); +const expand = __webpack_require__(791); +const parse = __webpack_require__(792); + +/** + * Expand the given pattern or create a regex-compatible string. + * + * ```js + * const braces = require('braces'); + * console.log(braces('{a,b,c}', { compile: true })); //=> ['(a|b|c)'] + * console.log(braces('{a,b,c}')); //=> ['a', 'b', 'c'] + * ``` + * @param {String} `str` + * @param {Object} `options` + * @return {String} + * @api public + */ + +const braces = (input, options = {}) => { + let output = []; + + if (Array.isArray(input)) { + for (let pattern of input) { + let result = braces.create(pattern, options); + if (Array.isArray(result)) { + output.push(...result); + } else { + output.push(result); + } + } + } else { + output = [].concat(braces.create(input, options)); + } + + if (options && options.expand === true && options.nodupes === true) { + output = [...new Set(output)]; + } + return output; +}; + +/** + * Parse the given `str` with the given `options`. + * + * ```js + * // braces.parse(pattern, [, options]); + * const ast = braces.parse('a/{b,c}/d'); + * console.log(ast); + * ``` + * @param {String} pattern Brace pattern to parse + * @param {Object} options + * @return {Object} Returns an AST + * @api public + */ + +braces.parse = (input, options = {}) => parse(input, options); + +/** + * Creates a braces string from an AST, or an AST node. + * + * ```js + * const braces = require('braces'); + * let ast = braces.parse('foo/{a,b}/bar'); + * console.log(stringify(ast.nodes[2])); //=> '{a,b}' + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.stringify = (input, options = {}) => { + if (typeof input === 'string') { + return stringify(braces.parse(input, options), options); + } + return stringify(input, options); +}; + +/** + * Compiles a brace pattern into a regex-compatible, optimized string. + * This method is called by the main [braces](#braces) function by default. + * + * ```js + * const braces = require('braces'); + * console.log(braces.compile('a/{b,c}/d')); + * //=> ['a/(b|c)/d'] + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.compile = (input, options = {}) => { + if (typeof input === 'string') { + input = braces.parse(input, options); + } + return compile(input, options); +}; + +/** + * Expands a brace pattern into an array. This method is called by the + * main [braces](#braces) function when `options.expand` is true. Before + * using this method it's recommended that you read the [performance notes](#performance)) + * and advantages of using [.compile](#compile) instead. + * + * ```js + * const braces = require('braces'); + * console.log(braces.expand('a/{b,c}/d')); + * //=> ['a/b/d', 'a/c/d']; + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.expand = (input, options = {}) => { + if (typeof input === 'string') { + input = braces.parse(input, options); + } + + let result = expand(input, options); + + // filter out empty strings if specified + if (options.noempty === true) { + result = result.filter(Boolean); + } + + // filter out duplicates if specified + if (options.nodupes === true) { + result = [...new Set(result)]; + } + + return result; +}; + +/** + * Processes a brace pattern and returns either an expanded array + * (if `options.expand` is true), a highly optimized regex-compatible string. + * This method is called by the main [braces](#braces) function. + * + * ```js + * const braces = require('braces'); + * console.log(braces.create('user-{200..300}/project-{a,b,c}-{1..10}')) + * //=> 'user-(20[0-9]|2[1-9][0-9]|300)/project-(a|b|c)-([1-9]|10)' + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.create = (input, options = {}) => { + if (input === '' || input.length < 3) { + return [input]; + } + + return options.expand !== true + ? braces.compile(input, options) + : braces.expand(input, options); +}; + +/** + * Expose "braces" + */ + +module.exports = braces; + + +/***/ }), +/* 785 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const utils = __webpack_require__(786); + +module.exports = (ast, options = {}) => { + let stringify = (node, parent = {}) => { + let invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let output = ''; + + if (node.value) { + if ((invalidBlock || invalidNode) && utils.isOpenOrClose(node)) { + return '\\' + node.value; + } + return node.value; + } + + if (node.value) { + return node.value; + } + + if (node.nodes) { + for (let child of node.nodes) { + output += stringify(child); + } + } + return output; + }; + + return stringify(ast); +}; + + + +/***/ }), +/* 786 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.isInteger = num => { + if (typeof num === 'number') { + return Number.isInteger(num); + } + if (typeof num === 'string' && num.trim() !== '') { + return Number.isInteger(Number(num)); + } + return false; +}; + +/** + * Find a node of the given type + */ + +exports.find = (node, type) => node.nodes.find(node => node.type === type); + +/** + * Find a node of the given type + */ + +exports.exceedsLimit = (min, max, step = 1, limit) => { + if (limit === false) return false; + if (!exports.isInteger(min) || !exports.isInteger(max)) return false; + return ((Number(max) - Number(min)) / Number(step)) >= limit; +}; + +/** + * Escape the given node with '\\' before node.value + */ + +exports.escapeNode = (block, n = 0, type) => { + let node = block.nodes[n]; + if (!node) return; + + if ((type && node.type === type) || node.type === 'open' || node.type === 'close') { + if (node.escaped !== true) { + node.value = '\\' + node.value; + node.escaped = true; + } + } +}; + +/** + * Returns true if the given brace node should be enclosed in literal braces + */ + +exports.encloseBrace = node => { + if (node.type !== 'brace') return false; + if ((node.commas >> 0 + node.ranges >> 0) === 0) { + node.invalid = true; + return true; + } + return false; +}; + +/** + * Returns true if a brace node is invalid. + */ + +exports.isInvalidBrace = block => { + if (block.type !== 'brace') return false; + if (block.invalid === true || block.dollar) return true; + if ((block.commas >> 0 + block.ranges >> 0) === 0) { + block.invalid = true; + return true; + } + if (block.open !== true || block.close !== true) { + block.invalid = true; + return true; + } + return false; +}; + +/** + * Returns true if a node is an open or close node + */ + +exports.isOpenOrClose = node => { + if (node.type === 'open' || node.type === 'close') { + return true; + } + return node.open === true || node.close === true; +}; + +/** + * Reduce an array of text nodes. + */ + +exports.reduce = nodes => nodes.reduce((acc, node) => { + if (node.type === 'text') acc.push(node.value); + if (node.type === 'range') node.type = 'text'; + return acc; +}, []); + +/** + * Flatten an array + */ + +exports.flatten = (...args) => { + const result = []; + const flat = arr => { + for (let i = 0; i < arr.length; i++) { + let ele = arr[i]; + Array.isArray(ele) ? flat(ele, result) : ele !== void 0 && result.push(ele); + } + return result; + }; + flat(args); + return result; +}; + + +/***/ }), +/* 787 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const fill = __webpack_require__(788); +const utils = __webpack_require__(786); + +const compile = (ast, options = {}) => { + let walk = (node, parent = {}) => { + let invalidBlock = utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let invalid = invalidBlock === true || invalidNode === true; + let prefix = options.escapeInvalid === true ? '\\' : ''; + let output = ''; + + if (node.isOpen === true) { + return prefix + node.value; + } + if (node.isClose === true) { + return prefix + node.value; + } + + if (node.type === 'open') { + return invalid ? (prefix + node.value) : '('; + } + + if (node.type === 'close') { + return invalid ? (prefix + node.value) : ')'; + } + + if (node.type === 'comma') { + return node.prev.type === 'comma' ? '' : (invalid ? node.value : '|'); + } + + if (node.value) { + return node.value; + } + + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + let range = fill(...args, { ...options, wrap: false, toRegex: true }); + + if (range.length !== 0) { + return args.length > 1 && range.length > 1 ? `(${range})` : range; + } + } + + if (node.nodes) { + for (let child of node.nodes) { + output += walk(child, node); + } + } + return output; + }; + + return walk(ast); +}; + +module.exports = compile; + + +/***/ }), +/* 788 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +const util = __webpack_require__(112); +const toRegexRange = __webpack_require__(789); + +const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); + +const transform = toNumber => { + return value => toNumber === true ? Number(value) : String(value); +}; + +const isValidValue = value => { + return typeof value === 'number' || (typeof value === 'string' && value !== ''); +}; + +const isNumber = num => Number.isInteger(+num); + +const zeros = input => { + let value = `${input}`; + let index = -1; + if (value[0] === '-') value = value.slice(1); + if (value === '0') return false; + while (value[++index] === '0'); + return index > 0; +}; + +const stringify = (start, end, options) => { + if (typeof start === 'string' || typeof end === 'string') { + return true; + } + return options.stringify === true; +}; + +const pad = (input, maxLength, toNumber) => { + if (maxLength > 0) { + let dash = input[0] === '-' ? '-' : ''; + if (dash) input = input.slice(1); + input = (dash + input.padStart(dash ? maxLength - 1 : maxLength, '0')); + } + if (toNumber === false) { + return String(input); + } + return input; +}; + +const toMaxLen = (input, maxLength) => { + let negative = input[0] === '-' ? '-' : ''; + if (negative) { + input = input.slice(1); + maxLength--; + } + while (input.length < maxLength) input = '0' + input; + return negative ? ('-' + input) : input; +}; + +const toSequence = (parts, options) => { + parts.negatives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + parts.positives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + + let prefix = options.capture ? '' : '?:'; + let positives = ''; + let negatives = ''; + let result; + + if (parts.positives.length) { + positives = parts.positives.join('|'); + } + + if (parts.negatives.length) { + negatives = `-(${prefix}${parts.negatives.join('|')})`; + } + + if (positives && negatives) { + result = `${positives}|${negatives}`; + } else { + result = positives || negatives; + } + + if (options.wrap) { + return `(${prefix}${result})`; + } + + return result; +}; + +const toRange = (a, b, isNumbers, options) => { + if (isNumbers) { + return toRegexRange(a, b, { wrap: false, ...options }); + } + + let start = String.fromCharCode(a); + if (a === b) return start; + + let stop = String.fromCharCode(b); + return `[${start}-${stop}]`; +}; + +const toRegex = (start, end, options) => { + if (Array.isArray(start)) { + let wrap = options.wrap === true; + let prefix = options.capture ? '' : '?:'; + return wrap ? `(${prefix}${start.join('|')})` : start.join('|'); + } + return toRegexRange(start, end, options); +}; + +const rangeError = (...args) => { + return new RangeError('Invalid range arguments: ' + util.inspect(...args)); +}; + +const invalidRange = (start, end, options) => { + if (options.strictRanges === true) throw rangeError([start, end]); + return []; +}; + +const invalidStep = (step, options) => { + if (options.strictRanges === true) { + throw new TypeError(`Expected step "${step}" to be a number`); + } + return []; +}; + +const fillNumbers = (start, end, step = 1, options = {}) => { + let a = Number(start); + let b = Number(end); + + if (!Number.isInteger(a) || !Number.isInteger(b)) { + if (options.strictRanges === true) throw rangeError([start, end]); + return []; + } + + // fix negative zero + if (a === 0) a = 0; + if (b === 0) b = 0; + + let descending = a > b; + let startString = String(start); + let endString = String(end); + let stepString = String(step); + step = Math.max(Math.abs(step), 1); + + let padded = zeros(startString) || zeros(endString) || zeros(stepString); + let maxLen = padded ? Math.max(startString.length, endString.length, stepString.length) : 0; + let toNumber = padded === false && stringify(start, end, options) === false; + let format = options.transform || transform(toNumber); + + if (options.toRegex && step === 1) { + return toRange(toMaxLen(start, maxLen), toMaxLen(end, maxLen), true, options); + } + + let parts = { negatives: [], positives: [] }; + let push = num => parts[num < 0 ? 'negatives' : 'positives'].push(Math.abs(num)); + let range = []; + let index = 0; + + while (descending ? a >= b : a <= b) { + if (options.toRegex === true && step > 1) { + push(a); + } else { + range.push(pad(format(a, index), maxLen, toNumber)); + } + a = descending ? a - step : a + step; + index++; + } + + if (options.toRegex === true) { + return step > 1 + ? toSequence(parts, options) + : toRegex(range, null, { wrap: false, ...options }); + } + + return range; +}; + +const fillLetters = (start, end, step = 1, options = {}) => { + if ((!isNumber(start) && start.length > 1) || (!isNumber(end) && end.length > 1)) { + return invalidRange(start, end, options); + } + + + let format = options.transform || (val => String.fromCharCode(val)); + let a = `${start}`.charCodeAt(0); + let b = `${end}`.charCodeAt(0); + + let descending = a > b; + let min = Math.min(a, b); + let max = Math.max(a, b); + + if (options.toRegex && step === 1) { + return toRange(min, max, false, options); + } + + let range = []; + let index = 0; + + while (descending ? a >= b : a <= b) { + range.push(format(a, index)); + a = descending ? a - step : a + step; + index++; + } + + if (options.toRegex === true) { + return toRegex(range, null, { wrap: false, options }); + } + + return range; +}; + +const fill = (start, end, step, options = {}) => { + if (end == null && isValidValue(start)) { + return [start]; + } + + if (!isValidValue(start) || !isValidValue(end)) { + return invalidRange(start, end, options); + } + + if (typeof step === 'function') { + return fill(start, end, 1, { transform: step }); + } + + if (isObject(step)) { + return fill(start, end, 0, step); + } + + let opts = { ...options }; + if (opts.capture === true) opts.wrap = true; + step = step || opts.step || 1; + + if (!isNumber(step)) { + if (step != null && !isObject(step)) return invalidStep(step, opts); + return fill(start, end, 1, step); + } + + if (isNumber(start) && isNumber(end)) { + return fillNumbers(start, end, step, opts); + } + + return fillLetters(start, end, Math.max(Math.abs(step), 1), opts); +}; + +module.exports = fill; + + +/***/ }), +/* 789 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ + + + +const isNumber = __webpack_require__(790); + +const toRegexRange = (min, max, options) => { + if (isNumber(min) === false) { + throw new TypeError('toRegexRange: expected the first argument to be a number'); + } + + if (max === void 0 || min === max) { + return String(min); + } + + if (isNumber(max) === false) { + throw new TypeError('toRegexRange: expected the second argument to be a number.'); + } + + let opts = { relaxZeros: true, ...options }; + if (typeof opts.strictZeros === 'boolean') { + opts.relaxZeros = opts.strictZeros === false; + } + + let relax = String(opts.relaxZeros); + let shorthand = String(opts.shorthand); + let capture = String(opts.capture); + let wrap = String(opts.wrap); + let cacheKey = min + ':' + max + '=' + relax + shorthand + capture + wrap; + + if (toRegexRange.cache.hasOwnProperty(cacheKey)) { + return toRegexRange.cache[cacheKey].result; + } + + let a = Math.min(min, max); + let b = Math.max(min, max); + + if (Math.abs(a - b) === 1) { + let result = min + '|' + max; + if (opts.capture) { + return `(${result})`; + } + if (opts.wrap === false) { + return result; + } + return `(?:${result})`; + } + + let isPadded = hasPadding(min) || hasPadding(max); + let state = { min, max, a, b }; + let positives = []; + let negatives = []; + + if (isPadded) { + state.isPadded = isPadded; + state.maxLen = String(state.max).length; + } + + if (a < 0) { + let newMin = b < 0 ? Math.abs(b) : 1; + negatives = splitToPatterns(newMin, Math.abs(a), state, opts); + a = state.a = 0; + } + + if (b >= 0) { + positives = splitToPatterns(a, b, state, opts); + } + + state.negatives = negatives; + state.positives = positives; + state.result = collatePatterns(negatives, positives, opts); + + if (opts.capture === true) { + state.result = `(${state.result})`; + } else if (opts.wrap !== false && (positives.length + negatives.length) > 1) { + state.result = `(?:${state.result})`; + } + + toRegexRange.cache[cacheKey] = state; + return state.result; +}; + +function collatePatterns(neg, pos, options) { + let onlyNegative = filterPatterns(neg, pos, '-', false, options) || []; + let onlyPositive = filterPatterns(pos, neg, '', false, options) || []; + let intersected = filterPatterns(neg, pos, '-?', true, options) || []; + let subpatterns = onlyNegative.concat(intersected).concat(onlyPositive); + return subpatterns.join('|'); +} + +function splitToRanges(min, max) { + let nines = 1; + let zeros = 1; + + let stop = countNines(min, nines); + let stops = new Set([max]); + + while (min <= stop && stop <= max) { + stops.add(stop); + nines += 1; + stop = countNines(min, nines); + } + + stop = countZeros(max + 1, zeros) - 1; + + while (min < stop && stop <= max) { + stops.add(stop); + zeros += 1; + stop = countZeros(max + 1, zeros) - 1; + } + + stops = [...stops]; + stops.sort(compare); + return stops; +} + +/** + * Convert a range to a regex pattern + * @param {Number} `start` + * @param {Number} `stop` + * @return {String} + */ + +function rangeToPattern(start, stop, options) { + if (start === stop) { + return { pattern: start, count: [], digits: 0 }; + } + + let zipped = zip(start, stop); + let digits = zipped.length; + let pattern = ''; + let count = 0; + + for (let i = 0; i < digits; i++) { + let [startDigit, stopDigit] = zipped[i]; + + if (startDigit === stopDigit) { + pattern += startDigit; + + } else if (startDigit !== '0' || stopDigit !== '9') { + pattern += toCharacterClass(startDigit, stopDigit, options); + + } else { + count++; + } + } + + if (count) { + pattern += options.shorthand === true ? '\\d' : '[0-9]'; + } + + return { pattern, count: [count], digits }; +} + +function splitToPatterns(min, max, tok, options) { + let ranges = splitToRanges(min, max); + let tokens = []; + let start = min; + let prev; + + for (let i = 0; i < ranges.length; i++) { + let max = ranges[i]; + let obj = rangeToPattern(String(start), String(max), options); + let zeros = ''; + + if (!tok.isPadded && prev && prev.pattern === obj.pattern) { + if (prev.count.length > 1) { + prev.count.pop(); + } + + prev.count.push(obj.count[0]); + prev.string = prev.pattern + toQuantifier(prev.count); + start = max + 1; + continue; + } + + if (tok.isPadded) { + zeros = padZeros(max, tok, options); + } + + obj.string = zeros + obj.pattern + toQuantifier(obj.count); + tokens.push(obj); + start = max + 1; + prev = obj; + } + + return tokens; +} + +function filterPatterns(arr, comparison, prefix, intersection, options) { + let result = []; + + for (let ele of arr) { + let { string } = ele; + + // only push if _both_ are negative... + if (!intersection && !contains(comparison, 'string', string)) { + result.push(prefix + string); + } + + // or _both_ are positive + if (intersection && contains(comparison, 'string', string)) { + result.push(prefix + string); + } + } + return result; +} + +/** + * Zip strings + */ + +function zip(a, b) { + let arr = []; + for (let i = 0; i < a.length; i++) arr.push([a[i], b[i]]); + return arr; +} + +function compare(a, b) { + return a > b ? 1 : b > a ? -1 : 0; +} + +function contains(arr, key, val) { + return arr.some(ele => ele[key] === val); +} + +function countNines(min, len) { + return Number(String(min).slice(0, -len) + '9'.repeat(len)); +} + +function countZeros(integer, zeros) { + return integer - (integer % Math.pow(10, zeros)); +} + +function toQuantifier(digits) { + let [start = 0, stop = ''] = digits; + if (stop || start > 1) { + return `{${start + (stop ? ',' + stop : '')}}`; + } + return ''; +} + +function toCharacterClass(a, b, options) { + return `[${a}${(b - a === 1) ? '' : '-'}${b}]`; +} + +function hasPadding(str) { + return /^-?(0+)\d/.test(str); +} + +function padZeros(value, tok, options) { + if (!tok.isPadded) { + return value; + } + + let diff = Math.abs(tok.maxLen - String(value).length); + let relax = options.relaxZeros !== false; + + switch (diff) { + case 0: + return ''; + case 1: + return relax ? '0?' : '0'; + case 2: + return relax ? '0{0,2}' : '00'; + default: { + return relax ? `0{0,${diff}}` : `0{${diff}}`; + } + } +} + +/** + * Cache + */ + +toRegexRange.cache = {}; +toRegexRange.clearCache = () => (toRegexRange.cache = {}); + +/** + * Expose `toRegexRange` + */ + +module.exports = toRegexRange; + + +/***/ }), +/* 790 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ + + + +module.exports = function(num) { + if (typeof num === 'number') { + return num - num === 0; + } + if (typeof num === 'string' && num.trim() !== '') { + return Number.isFinite ? Number.isFinite(+num) : isFinite(+num); + } + return false; +}; + + +/***/ }), +/* 791 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const fill = __webpack_require__(788); +const stringify = __webpack_require__(785); +const utils = __webpack_require__(786); + +const append = (queue = '', stash = '', enclose = false) => { + let result = []; + + queue = [].concat(queue); + stash = [].concat(stash); + + if (!stash.length) return queue; + if (!queue.length) { + return enclose ? utils.flatten(stash).map(ele => `{${ele}}`) : stash; + } + + for (let item of queue) { + if (Array.isArray(item)) { + for (let value of item) { + result.push(append(value, stash, enclose)); + } + } else { + for (let ele of stash) { + if (enclose === true && typeof ele === 'string') ele = `{${ele}}`; + result.push(Array.isArray(ele) ? append(item, ele, enclose) : (item + ele)); + } + } + } + return utils.flatten(result); +}; + +const expand = (ast, options = {}) => { + let rangeLimit = options.rangeLimit === void 0 ? 1000 : options.rangeLimit; + + let walk = (node, parent = {}) => { + node.queue = []; + + let p = parent; + let q = parent.queue; + + while (p.type !== 'brace' && p.type !== 'root' && p.parent) { + p = p.parent; + q = p.queue; + } + + if (node.invalid || node.dollar) { + q.push(append(q.pop(), stringify(node, options))); + return; + } + + if (node.type === 'brace' && node.invalid !== true && node.nodes.length === 2) { + q.push(append(q.pop(), ['{}'])); + return; + } + + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + + if (utils.exceedsLimit(...args, options.step, rangeLimit)) { + throw new RangeError('expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.'); + } + + let range = fill(...args, options); + if (range.length === 0) { + range = stringify(node, options); + } + + q.push(append(q.pop(), range)); + node.nodes = []; + return; + } + + let enclose = utils.encloseBrace(node); + let queue = node.queue; + let block = node; + + while (block.type !== 'brace' && block.type !== 'root' && block.parent) { + block = block.parent; + queue = block.queue; + } + + for (let i = 0; i < node.nodes.length; i++) { + let child = node.nodes[i]; + + if (child.type === 'comma' && node.type === 'brace') { + if (i === 1) queue.push(''); + queue.push(''); + continue; + } + + if (child.type === 'close') { + q.push(append(q.pop(), queue, enclose)); + continue; + } + + if (child.value && child.type !== 'open') { + queue.push(append(queue.pop(), child.value)); + continue; + } + + if (child.nodes) { + walk(child, node); + } + } + + return queue; + }; + + return utils.flatten(walk(ast)); +}; + +module.exports = expand; + + +/***/ }), +/* 792 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const stringify = __webpack_require__(785); + +/** + * Constants + */ + +const { + MAX_LENGTH, + CHAR_BACKSLASH, /* \ */ + CHAR_BACKTICK, /* ` */ + CHAR_COMMA, /* , */ + CHAR_DOT, /* . */ + CHAR_LEFT_PARENTHESES, /* ( */ + CHAR_RIGHT_PARENTHESES, /* ) */ + CHAR_LEFT_CURLY_BRACE, /* { */ + CHAR_RIGHT_CURLY_BRACE, /* } */ + CHAR_LEFT_SQUARE_BRACKET, /* [ */ + CHAR_RIGHT_SQUARE_BRACKET, /* ] */ + CHAR_DOUBLE_QUOTE, /* " */ + CHAR_SINGLE_QUOTE, /* ' */ + CHAR_NO_BREAK_SPACE, + CHAR_ZERO_WIDTH_NOBREAK_SPACE +} = __webpack_require__(793); + +/** + * parse + */ + +const parse = (input, options = {}) => { + if (typeof input !== 'string') { + throw new TypeError('Expected a string'); + } + + let opts = options || {}; + let max = typeof opts.maxLength === 'number' ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; + if (input.length > max) { + throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`); + } + + let ast = { type: 'root', input, nodes: [] }; + let stack = [ast]; + let block = ast; + let prev = ast; + let brackets = 0; + let length = input.length; + let index = 0; + let depth = 0; + let value; + let memo = {}; + + /** + * Helpers + */ + + const advance = () => input[index++]; + const push = node => { + if (node.type === 'text' && prev.type === 'dot') { + prev.type = 'text'; + } + + if (prev && prev.type === 'text' && node.type === 'text') { + prev.value += node.value; + return; + } + + block.nodes.push(node); + node.parent = block; + node.prev = prev; + prev = node; + return node; + }; + + push({ type: 'bos' }); + + while (index < length) { + block = stack[stack.length - 1]; + value = advance(); + + /** + * Invalid chars + */ + + if (value === CHAR_ZERO_WIDTH_NOBREAK_SPACE || value === CHAR_NO_BREAK_SPACE) { + continue; + } + + /** + * Escaped chars + */ + + if (value === CHAR_BACKSLASH) { + push({ type: 'text', value: (options.keepEscaping ? value : '') + advance() }); + continue; + } + + /** + * Right square bracket (literal): ']' + */ + + if (value === CHAR_RIGHT_SQUARE_BRACKET) { + push({ type: 'text', value: '\\' + value }); + continue; + } + + /** + * Left square bracket: '[' + */ + + if (value === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + + let closed = true; + let next; + + while (index < length && (next = advance())) { + value += next; + + if (next === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + continue; + } + + if (next === CHAR_BACKSLASH) { + value += advance(); + continue; + } + + if (next === CHAR_RIGHT_SQUARE_BRACKET) { + brackets--; + + if (brackets === 0) { + break; + } + } + } + + push({ type: 'text', value }); + continue; + } + + /** + * Parentheses + */ + + if (value === CHAR_LEFT_PARENTHESES) { + block = push({ type: 'paren', nodes: [] }); + stack.push(block); + push({ type: 'text', value }); + continue; + } + + if (value === CHAR_RIGHT_PARENTHESES) { + if (block.type !== 'paren') { + push({ type: 'text', value }); + continue; + } + block = stack.pop(); + push({ type: 'text', value }); + block = stack[stack.length - 1]; + continue; + } + + /** + * Quotes: '|"|` + */ + + if (value === CHAR_DOUBLE_QUOTE || value === CHAR_SINGLE_QUOTE || value === CHAR_BACKTICK) { + let open = value; + let next; + + if (options.keepQuotes !== true) { + value = ''; + } + + while (index < length && (next = advance())) { + if (next === CHAR_BACKSLASH) { + value += next + advance(); + continue; + } + + if (next === open) { + if (options.keepQuotes === true) value += next; + break; + } + + value += next; + } + + push({ type: 'text', value }); + continue; + } + + /** + * Left curly brace: '{' + */ + + if (value === CHAR_LEFT_CURLY_BRACE) { + depth++; + + let dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true; + let brace = { + type: 'brace', + open: true, + close: false, + dollar, + depth, + commas: 0, + ranges: 0, + nodes: [] + }; + + block = push(brace); + stack.push(block); + push({ type: 'open', value }); + continue; + } + + /** + * Right curly brace: '}' + */ + + if (value === CHAR_RIGHT_CURLY_BRACE) { + if (block.type !== 'brace') { + push({ type: 'text', value }); + continue; + } + + let type = 'close'; + block = stack.pop(); + block.close = true; + + push({ type, value }); + depth--; + + block = stack[stack.length - 1]; + continue; + } + + /** + * Comma: ',' + */ + + if (value === CHAR_COMMA && depth > 0) { + if (block.ranges > 0) { + block.ranges = 0; + let open = block.nodes.shift(); + block.nodes = [open, { type: 'text', value: stringify(block) }]; + } + + push({ type: 'comma', value }); + block.commas++; + continue; + } + + /** + * Dot: '.' + */ + + if (value === CHAR_DOT && depth > 0 && block.commas === 0) { + let siblings = block.nodes; + + if (depth === 0 || siblings.length === 0) { + push({ type: 'text', value }); + continue; + } + + if (prev.type === 'dot') { + block.range = []; + prev.value += value; + prev.type = 'range'; + + if (block.nodes.length !== 3 && block.nodes.length !== 5) { + block.invalid = true; + block.ranges = 0; + prev.type = 'text'; + continue; + } + + block.ranges++; + block.args = []; + continue; + } + + if (prev.type === 'range') { + siblings.pop(); + + let before = siblings[siblings.length - 1]; + before.value += prev.value + value; + prev = before; + block.ranges--; + continue; + } + + push({ type: 'dot', value }); + continue; + } + + /** + * Text + */ + + push({ type: 'text', value }); + } + + // Mark imbalanced braces and brackets as invalid + do { + block = stack.pop(); + + if (block.type !== 'root') { + block.nodes.forEach(node => { + if (!node.nodes) { + if (node.type === 'open') node.isOpen = true; + if (node.type === 'close') node.isClose = true; + if (!node.nodes) node.type = 'text'; + node.invalid = true; + } + }); + + // get the location of the block on parent.nodes (block's siblings) + let parent = stack[stack.length - 1]; + let index = parent.nodes.indexOf(block); + // replace the (invalid) block with it's nodes + parent.nodes.splice(index, 1, ...block.nodes); + } + } while (stack.length > 0); + + push({ type: 'eos' }); + return ast; +}; + +module.exports = parse; + + +/***/ }), +/* 793 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = { + MAX_LENGTH: 1024 * 64, + + // Digits + CHAR_0: '0', /* 0 */ + CHAR_9: '9', /* 9 */ + + // Alphabet chars. + CHAR_UPPERCASE_A: 'A', /* A */ + CHAR_LOWERCASE_A: 'a', /* a */ + CHAR_UPPERCASE_Z: 'Z', /* Z */ + CHAR_LOWERCASE_Z: 'z', /* z */ + + CHAR_LEFT_PARENTHESES: '(', /* ( */ + CHAR_RIGHT_PARENTHESES: ')', /* ) */ + + CHAR_ASTERISK: '*', /* * */ + + // Non-alphabetic chars. + CHAR_AMPERSAND: '&', /* & */ + CHAR_AT: '@', /* @ */ + CHAR_BACKSLASH: '\\', /* \ */ + CHAR_BACKTICK: '`', /* ` */ + CHAR_CARRIAGE_RETURN: '\r', /* \r */ + CHAR_CIRCUMFLEX_ACCENT: '^', /* ^ */ + CHAR_COLON: ':', /* : */ + CHAR_COMMA: ',', /* , */ + CHAR_DOLLAR: '$', /* . */ + CHAR_DOT: '.', /* . */ + CHAR_DOUBLE_QUOTE: '"', /* " */ + CHAR_EQUAL: '=', /* = */ + CHAR_EXCLAMATION_MARK: '!', /* ! */ + CHAR_FORM_FEED: '\f', /* \f */ + CHAR_FORWARD_SLASH: '/', /* / */ + CHAR_HASH: '#', /* # */ + CHAR_HYPHEN_MINUS: '-', /* - */ + CHAR_LEFT_ANGLE_BRACKET: '<', /* < */ + CHAR_LEFT_CURLY_BRACE: '{', /* { */ + CHAR_LEFT_SQUARE_BRACKET: '[', /* [ */ + CHAR_LINE_FEED: '\n', /* \n */ + CHAR_NO_BREAK_SPACE: '\u00A0', /* \u00A0 */ + CHAR_PERCENT: '%', /* % */ + CHAR_PLUS: '+', /* + */ + CHAR_QUESTION_MARK: '?', /* ? */ + CHAR_RIGHT_ANGLE_BRACKET: '>', /* > */ + CHAR_RIGHT_CURLY_BRACE: '}', /* } */ + CHAR_RIGHT_SQUARE_BRACKET: ']', /* ] */ + CHAR_SEMICOLON: ';', /* ; */ + CHAR_SINGLE_QUOTE: '\'', /* ' */ + CHAR_SPACE: ' ', /* */ + CHAR_TAB: '\t', /* \t */ + CHAR_UNDERSCORE: '_', /* _ */ + CHAR_VERTICAL_LINE: '|', /* | */ + CHAR_ZERO_WIDTH_NOBREAK_SPACE: '\uFEFF' /* \uFEFF */ +}; + + +/***/ }), +/* 794 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.merge = void 0; +const merge2 = __webpack_require__(146); +function merge(streams) { + const mergedStream = merge2(streams); + streams.forEach((stream) => { + stream.once('error', (error) => mergedStream.emit('error', error)); + }); + mergedStream.once('close', () => propagateCloseEventToSources(streams)); + mergedStream.once('end', () => propagateCloseEventToSources(streams)); + return mergedStream; +} +exports.merge = merge; +function propagateCloseEventToSources(streams) { + streams.forEach((stream) => stream.emit('close')); +} + + +/***/ }), +/* 795 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isEmpty = exports.isString = void 0; +function isString(input) { + return typeof input === 'string'; +} +exports.isString = isString; +function isEmpty(input) { + return input === ''; +} +exports.isEmpty = isEmpty; + + +/***/ }), +/* 796 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(797); +const provider_1 = __webpack_require__(799); +class ProviderAsync extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new stream_1.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const entries = []; + return new Promise((resolve, reject) => { + const stream = this.api(root, task, options); + stream.once('error', reject); + stream.on('data', (entry) => entries.push(options.transform(entry))); + stream.once('end', () => resolve(entries)); + }); + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderAsync; + + +/***/ }), +/* 797 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(138); +const fsStat = __webpack_require__(195); +const fsWalk = __webpack_require__(200); +const reader_1 = __webpack_require__(798); +class ReaderStream extends reader_1.default { + constructor() { + super(...arguments); + this._walkStream = fsWalk.walkStream; + this._stat = fsStat.stat; + } + dynamic(root, options) { + return this._walkStream(root, options); + } + static(patterns, options) { + const filepaths = patterns.map(this._getFullEntryPath, this); + const stream = new stream_1.PassThrough({ objectMode: true }); + stream._write = (index, _enc, done) => { + return this._getEntry(filepaths[index], patterns[index], options) + .then((entry) => { + if (entry !== null && options.entryFilter(entry)) { + stream.push(entry); + } + if (index === filepaths.length - 1) { + stream.end(); + } + done(); + }) + .catch(done); + }; + for (let i = 0; i < filepaths.length; i++) { + stream.write(i); + } + return stream; + } + _getEntry(filepath, pattern, options) { + return this._getStat(filepath) + .then((stats) => this._makeEntry(stats, pattern)) + .catch((error) => { + if (options.errorFilter(error)) { + return null; + } + throw error; + }); + } + _getStat(filepath) { + return new Promise((resolve, reject) => { + this._stat(filepath, this._fsStatSettings, (error, stats) => { + return error === null ? resolve(stats) : reject(error); + }); + }); + } +} +exports.default = ReaderStream; + + +/***/ }), +/* 798 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __webpack_require__(4); +const fsStat = __webpack_require__(195); +const utils = __webpack_require__(777); +class Reader { + constructor(_settings) { + this._settings = _settings; + this._fsStatSettings = new fsStat.Settings({ + followSymbolicLink: this._settings.followSymbolicLinks, + fs: this._settings.fs, + throwErrorOnBrokenSymbolicLink: this._settings.followSymbolicLinks + }); + } + _getFullEntryPath(filepath) { + return path.resolve(this._settings.cwd, filepath); + } + _makeEntry(stats, pattern) { + const entry = { + name: pattern, + path: pattern, + dirent: utils.fs.createDirentFromStats(pattern, stats) + }; + if (this._settings.stats) { + entry.stats = stats; + } + return entry; + } + _isFatalError(error) { + return !utils.errno.isEnoentCodeError(error) && !this._settings.suppressErrors; + } +} +exports.default = Reader; + + +/***/ }), +/* 799 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __webpack_require__(4); +const deep_1 = __webpack_require__(800); +const entry_1 = __webpack_require__(803); +const error_1 = __webpack_require__(804); +const entry_2 = __webpack_require__(805); +class Provider { + constructor(_settings) { + this._settings = _settings; + this.errorFilter = new error_1.default(this._settings); + this.entryFilter = new entry_1.default(this._settings, this._getMicromatchOptions()); + this.deepFilter = new deep_1.default(this._settings, this._getMicromatchOptions()); + this.entryTransformer = new entry_2.default(this._settings); + } + _getRootDirectory(task) { + return path.resolve(this._settings.cwd, task.base); + } + _getReaderOptions(task) { + const basePath = task.base === '.' ? '' : task.base; + return { + basePath, + pathSegmentSeparator: '/', + concurrency: this._settings.concurrency, + deepFilter: this.deepFilter.getFilter(basePath, task.positive, task.negative), + entryFilter: this.entryFilter.getFilter(task.positive, task.negative), + errorFilter: this.errorFilter.getFilter(), + followSymbolicLinks: this._settings.followSymbolicLinks, + fs: this._settings.fs, + stats: this._settings.stats, + throwErrorOnBrokenSymbolicLink: this._settings.throwErrorOnBrokenSymbolicLink, + transform: this.entryTransformer.getTransformer() + }; + } + _getMicromatchOptions() { + return { + dot: this._settings.dot, + matchBase: this._settings.baseNameMatch, + nobrace: !this._settings.braceExpansion, + nocase: !this._settings.caseSensitiveMatch, + noext: !this._settings.extglob, + noglobstar: !this._settings.globstar, + posix: true, + strictSlashes: false + }; + } +} +exports.default = Provider; + + +/***/ }), +/* 800 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +const partial_1 = __webpack_require__(801); +class DeepFilter { + constructor(_settings, _micromatchOptions) { + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + } + getFilter(basePath, positive, negative) { + const matcher = this._getMatcher(positive); + const negativeRe = this._getNegativePatternsRe(negative); + return (entry) => this._filter(basePath, entry, matcher, negativeRe); + } + _getMatcher(patterns) { + return new partial_1.default(patterns, this._settings, this._micromatchOptions); + } + _getNegativePatternsRe(patterns) { + const affectDepthOfReadingPatterns = patterns.filter(utils.pattern.isAffectDepthOfReadingPattern); + return utils.pattern.convertPatternsToRe(affectDepthOfReadingPatterns, this._micromatchOptions); + } + _filter(basePath, entry, matcher, negativeRe) { + if (this._isSkippedByDeep(basePath, entry.path)) { + return false; + } + if (this._isSkippedSymbolicLink(entry)) { + return false; + } + const filepath = utils.path.removeLeadingDotSegment(entry.path); + if (this._isSkippedByPositivePatterns(filepath, matcher)) { + return false; + } + return this._isSkippedByNegativePatterns(filepath, negativeRe); + } + _isSkippedByDeep(basePath, entryPath) { + /** + * Avoid unnecessary depth calculations when it doesn't matter. + */ + if (this._settings.deep === Infinity) { + return false; + } + return this._getEntryLevel(basePath, entryPath) >= this._settings.deep; + } + _getEntryLevel(basePath, entryPath) { + const entryPathDepth = entryPath.split('/').length; + if (basePath === '') { + return entryPathDepth; + } + const basePathDepth = basePath.split('/').length; + return entryPathDepth - basePathDepth; + } + _isSkippedSymbolicLink(entry) { + return !this._settings.followSymbolicLinks && entry.dirent.isSymbolicLink(); + } + _isSkippedByPositivePatterns(entryPath, matcher) { + return !this._settings.baseNameMatch && !matcher.match(entryPath); + } + _isSkippedByNegativePatterns(entryPath, patternsRe) { + return !utils.pattern.matchAny(entryPath, patternsRe); + } +} +exports.default = DeepFilter; + + +/***/ }), +/* 801 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const matcher_1 = __webpack_require__(802); +class PartialMatcher extends matcher_1.default { + match(filepath) { + const parts = filepath.split('/'); + const levels = parts.length; + const patterns = this._storage.filter((info) => !info.complete || info.segments.length > levels); + for (const pattern of patterns) { + const section = pattern.sections[0]; + /** + * In this case, the pattern has a globstar and we must read all directories unconditionally, + * but only if the level has reached the end of the first group. + * + * fixtures/{a,b}/** + * ^ true/false ^ always true + */ + if (!pattern.complete && levels > section.length) { + return true; + } + const match = parts.every((part, index) => { + const segment = pattern.segments[index]; + if (segment.dynamic && segment.patternRe.test(part)) { + return true; + } + if (!segment.dynamic && segment.pattern === part) { + return true; + } + return false; + }); + if (match) { + return true; + } + } + return false; + } +} +exports.default = PartialMatcher; + + +/***/ }), +/* 802 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class Matcher { + constructor(_patterns, _settings, _micromatchOptions) { + this._patterns = _patterns; + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + this._storage = []; + this._fillStorage(); + } + _fillStorage() { + /** + * The original pattern may include `{,*,**,a/*}`, which will lead to problems with matching (unresolved level). + * So, before expand patterns with brace expansion into separated patterns. + */ + const patterns = utils.pattern.expandPatternsWithBraceExpansion(this._patterns); + for (const pattern of patterns) { + const segments = this._getPatternSegments(pattern); + const sections = this._splitSegmentsIntoSections(segments); + this._storage.push({ + complete: sections.length <= 1, + pattern, + segments, + sections + }); + } + } + _getPatternSegments(pattern) { + const parts = utils.pattern.getPatternParts(pattern, this._micromatchOptions); + return parts.map((part) => { + const dynamic = utils.pattern.isDynamicPattern(part, this._settings); + if (!dynamic) { + return { + dynamic: false, + pattern: part + }; + } + return { + dynamic: true, + pattern: part, + patternRe: utils.pattern.makeRe(part, this._micromatchOptions) + }; + }); + } + _splitSegmentsIntoSections(segments) { + return utils.array.splitWhen(segments, (segment) => segment.dynamic && utils.pattern.hasGlobStar(segment.pattern)); + } +} +exports.default = Matcher; + + +/***/ }), +/* 803 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class EntryFilter { + constructor(_settings, _micromatchOptions) { + this._settings = _settings; + this._micromatchOptions = _micromatchOptions; + this.index = new Map(); + } + getFilter(positive, negative) { + const positiveRe = utils.pattern.convertPatternsToRe(positive, this._micromatchOptions); + const negativeRe = utils.pattern.convertPatternsToRe(negative, this._micromatchOptions); + return (entry) => this._filter(entry, positiveRe, negativeRe); + } + _filter(entry, positiveRe, negativeRe) { + if (this._settings.unique && this._isDuplicateEntry(entry)) { + return false; + } + if (this._onlyFileFilter(entry) || this._onlyDirectoryFilter(entry)) { + return false; + } + if (this._isSkippedByAbsoluteNegativePatterns(entry.path, negativeRe)) { + return false; + } + const filepath = this._settings.baseNameMatch ? entry.name : entry.path; + const isMatched = this._isMatchToPatterns(filepath, positiveRe) && !this._isMatchToPatterns(entry.path, negativeRe); + if (this._settings.unique && isMatched) { + this._createIndexRecord(entry); + } + return isMatched; + } + _isDuplicateEntry(entry) { + return this.index.has(entry.path); + } + _createIndexRecord(entry) { + this.index.set(entry.path, undefined); + } + _onlyFileFilter(entry) { + return this._settings.onlyFiles && !entry.dirent.isFile(); + } + _onlyDirectoryFilter(entry) { + return this._settings.onlyDirectories && !entry.dirent.isDirectory(); + } + _isSkippedByAbsoluteNegativePatterns(entryPath, patternsRe) { + if (!this._settings.absolute) { + return false; + } + const fullpath = utils.path.makeAbsolute(this._settings.cwd, entryPath); + return utils.pattern.matchAny(fullpath, patternsRe); + } + _isMatchToPatterns(entryPath, patternsRe) { + const filepath = utils.path.removeLeadingDotSegment(entryPath); + return utils.pattern.matchAny(filepath, patternsRe); + } +} +exports.default = EntryFilter; - for (let i = 0; i < arguments.length; i++) { - results[i] = arguments[i]; - } - resolve(results); - } else { - resolve(result); - } - }); - } +/***/ }), +/* 804 */ +/***/ (function(module, exports, __webpack_require__) { - fn.apply(this, args); - }); -}; +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class ErrorFilter { + constructor(_settings) { + this._settings = _settings; + } + getFilter() { + return (error) => this._isNonFatalError(error); + } + _isNonFatalError(error) { + return utils.errno.isEnoentCodeError(error) || this._settings.suppressErrors; + } +} +exports.default = ErrorFilter; -module.exports = (obj, opts) => { - opts = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, opts); - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return opts.include ? opts.include.some(match) : !opts.exclude.some(match); - }; +/***/ }), +/* 805 */ +/***/ (function(module, exports, __webpack_require__) { - let ret; - if (typeof obj === 'function') { - ret = function () { - if (opts.excludeMain) { - return obj.apply(this, arguments); - } +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const utils = __webpack_require__(777); +class EntryTransformer { + constructor(_settings) { + this._settings = _settings; + } + getTransformer() { + return (entry) => this._transform(entry); + } + _transform(entry) { + let filepath = entry.path; + if (this._settings.absolute) { + filepath = utils.path.makeAbsolute(this._settings.cwd, filepath); + filepath = utils.path.unixify(filepath); + } + if (this._settings.markDirectories && entry.dirent.isDirectory()) { + filepath += '/'; + } + if (!this._settings.objectMode) { + return filepath; + } + return Object.assign(Object.assign({}, entry), { path: filepath }); + } +} +exports.default = EntryTransformer; - return processFn(obj, opts).apply(this, arguments); - }; - } else { - ret = Object.create(Object.getPrototypeOf(obj)); - } - for (const key in obj) { // eslint-disable-line guard-for-in - const x = obj[key]; - ret[key] = typeof x === 'function' && filter(key) ? processFn(x, opts) : x; - } +/***/ }), +/* 806 */ +/***/ (function(module, exports, __webpack_require__) { - return ret; -}; +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = __webpack_require__(138); +const stream_2 = __webpack_require__(797); +const provider_1 = __webpack_require__(799); +class ProviderStream extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new stream_2.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const source = this.api(root, task, options); + const destination = new stream_1.Readable({ objectMode: true, read: () => { } }); + source + .once('error', (error) => destination.emit('error', error)) + .on('data', (entry) => destination.emit('data', options.transform(entry))) + .once('end', () => destination.emit('end')); + destination + .once('close', () => source.destroy()); + return destination; + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderStream; /***/ }), -/* 780 */ +/* 807 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const sync_1 = __webpack_require__(808); +const provider_1 = __webpack_require__(799); +class ProviderSync extends provider_1.default { + constructor() { + super(...arguments); + this._reader = new sync_1.default(this._settings); + } + read(task) { + const root = this._getRootDirectory(task); + const options = this._getReaderOptions(task); + const entries = this.api(root, task, options); + return entries.map(options.transform); + } + api(root, task, options) { + if (task.dynamic) { + return this._reader.dynamic(root, options); + } + return this._reader.static(task.patterns, options); + } +} +exports.default = ProviderSync; + + +/***/ }), +/* 808 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +const fsStat = __webpack_require__(195); +const fsWalk = __webpack_require__(200); +const reader_1 = __webpack_require__(798); +class ReaderSync extends reader_1.default { + constructor() { + super(...arguments); + this._walkSync = fsWalk.walkSync; + this._statSync = fsStat.statSync; + } + dynamic(root, options) { + return this._walkSync(root, options); + } + static(patterns, options) { + const entries = []; + for (const pattern of patterns) { + const filepath = this._getFullEntryPath(pattern); + const entry = this._getEntry(filepath, pattern, options); + if (entry === null || !options.entryFilter(entry)) { + continue; + } + entries.push(entry); + } + return entries; + } + _getEntry(filepath, pattern, options) { + try { + const stats = this._getStat(filepath); + return this._makeEntry(stats, pattern); + } + catch (error) { + if (options.errorFilter(error)) { + return null; + } + throw error; + } + } + _getStat(filepath) { + return this._statSync(filepath, this._fsStatSettings); + } +} +exports.default = ReaderSync; + + +/***/ }), +/* 809 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = void 0; +const fs = __webpack_require__(134); +const os = __webpack_require__(121); +/** + * The `os.cpus` method can return zero. We expect the number of cores to be greater than zero. + * https://github.com/nodejs/node/blob/7faeddf23a98c53896f8b574a6e66589e8fb1eb8/lib/os.js#L106-L107 + */ +const CPU_COUNT = Math.max(os.cpus().length, 1); +exports.DEFAULT_FILE_SYSTEM_ADAPTER = { + lstat: fs.lstat, + lstatSync: fs.lstatSync, + stat: fs.stat, + statSync: fs.statSync, + readdir: fs.readdir, + readdirSync: fs.readdirSync +}; +class Settings { + constructor(_options = {}) { + this._options = _options; + this.absolute = this._getValue(this._options.absolute, false); + this.baseNameMatch = this._getValue(this._options.baseNameMatch, false); + this.braceExpansion = this._getValue(this._options.braceExpansion, true); + this.caseSensitiveMatch = this._getValue(this._options.caseSensitiveMatch, true); + this.concurrency = this._getValue(this._options.concurrency, CPU_COUNT); + this.cwd = this._getValue(this._options.cwd, process.cwd()); + this.deep = this._getValue(this._options.deep, Infinity); + this.dot = this._getValue(this._options.dot, false); + this.extglob = this._getValue(this._options.extglob, true); + this.followSymbolicLinks = this._getValue(this._options.followSymbolicLinks, true); + this.fs = this._getFileSystemMethods(this._options.fs); + this.globstar = this._getValue(this._options.globstar, true); + this.ignore = this._getValue(this._options.ignore, []); + this.markDirectories = this._getValue(this._options.markDirectories, false); + this.objectMode = this._getValue(this._options.objectMode, false); + this.onlyDirectories = this._getValue(this._options.onlyDirectories, false); + this.onlyFiles = this._getValue(this._options.onlyFiles, true); + this.stats = this._getValue(this._options.stats, false); + this.suppressErrors = this._getValue(this._options.suppressErrors, false); + this.throwErrorOnBrokenSymbolicLink = this._getValue(this._options.throwErrorOnBrokenSymbolicLink, false); + this.unique = this._getValue(this._options.unique, true); + if (this.onlyDirectories) { + this.onlyFiles = false; + } + if (this.stats) { + this.objectMode = true; + } + } + _getValue(option, value) { + return option === undefined ? value : option; + } + _getFileSystemMethods(methods = {}) { + return Object.assign(Object.assign({}, exports.DEFAULT_FILE_SYSTEM_ADAPTER), methods); + } +} +exports.default = Settings; + + +/***/ }), +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(572); -const gitIgnore = __webpack_require__(781); -const pify = __webpack_require__(779); -const slash = __webpack_require__(782); +const fastGlob = __webpack_require__(775); +const gitIgnore = __webpack_require__(235); +const slash = __webpack_require__(236); const DEFAULT_IGNORE = [ '**/node_modules/**', - '**/bower_components/**', '**/flow-typed/**', '**/coverage/**', '**/.git' ]; -const readFileP = pify(fs.readFile); +const readFileP = promisify(fs.readFile); const mapGitIgnorePatternTo = base => ignore => { if (ignore.startsWith('!')) { - return '!' + path.posix.join(base, ignore.substr(1)); + return '!' + path.posix.join(base, ignore.slice(1)); } return path.posix.join(base, ignore); }; -const parseGitIgnore = (content, opts) => { - const base = slash(path.relative(opts.cwd, path.dirname(opts.fileName))); +const parseGitIgnore = (content, options) => { + const base = slash(path.relative(options.cwd, path.dirname(options.fileName))); return content .split(/\r?\n/) .filter(Boolean) - .filter(l => l.charAt(0) !== '#') + .filter(line => !line.startsWith('#')) .map(mapGitIgnorePatternTo(base)); }; const reduceIgnore = files => { - return files.reduce((ignores, file) => { + const ignores = gitIgnore(); + for (const file of files) { ignores.add(parseGitIgnore(file.content, { cwd: file.cwd, fileName: file.filePath })); - return ignores; - }, gitIgnore()); + } + + return ignores; +}; + +const ensureAbsolutePathForCwd = (cwd, p) => { + cwd = slash(cwd); + if (path.isAbsolute(p)) { + if (slash(p).startsWith(cwd)) { + return p; + } + + throw new Error(`Path ${p} is not in cwd ${cwd}`); + } + + return path.join(cwd, p); }; const getIsIgnoredPredecate = (ignores, cwd) => { - return p => ignores.ignores(slash(path.relative(cwd, p))); + return p => ignores.ignores(slash(path.relative(cwd, ensureAbsolutePathForCwd(cwd, p.path || p)))); }; -const getFile = (file, cwd) => { +const getFile = async (file, cwd) => { const filePath = path.join(cwd, file); - return readFileP(filePath, 'utf8') - .then(content => ({ - content, - cwd, - filePath - })); + const content = await readFileP(filePath, 'utf8'); + + return { + cwd, + filePath, + content + }; }; const getFileSync = (file, cwd) => { @@ -90649,490 +93669,103 @@ const getFileSync = (file, cwd) => { const content = fs.readFileSync(filePath, 'utf8'); return { - content, cwd, - filePath + filePath, + content }; }; -const normalizeOpts = opts => { - opts = opts || {}; - const ignore = opts.ignore || []; - const cwd = opts.cwd || process.cwd(); +const normalizeOptions = ({ + ignore = [], + cwd = slash(process.cwd()) +} = {}) => { return {ignore, cwd}; }; -module.exports = o => { - const opts = normalizeOpts(o); - - return fastGlob('**/.gitignore', {ignore: DEFAULT_IGNORE.concat(opts.ignore), cwd: opts.cwd}) - .then(paths => Promise.all(paths.map(file => getFile(file, opts.cwd)))) - .then(files => reduceIgnore(files)) - .then(ignores => getIsIgnoredPredecate(ignores, opts.cwd)); -}; +module.exports = async options => { + options = normalizeOptions(options); -module.exports.sync = o => { - const opts = normalizeOpts(o); + const paths = await fastGlob('**/.gitignore', { + ignore: DEFAULT_IGNORE.concat(options.ignore), + cwd: options.cwd + }); - const paths = fastGlob.sync('**/.gitignore', {ignore: DEFAULT_IGNORE.concat(opts.ignore), cwd: opts.cwd}); - const files = paths.map(file => getFileSync(file, opts.cwd)); + const files = await Promise.all(paths.map(file => getFile(file, options.cwd))); const ignores = reduceIgnore(files); - return getIsIgnoredPredecate(ignores, opts.cwd); -}; - - -/***/ }), -/* 781 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -module.exports = function () { - return new IgnoreBase(); + return getIsIgnoredPredecate(ignores, options.cwd); }; -// A simple implementation of make-array -function make_array(subject) { - return Array.isArray(subject) ? subject : [subject]; -} - -var REGEX_BLANK_LINE = /^\s+$/; -var REGEX_LEADING_EXCAPED_EXCLAMATION = /^\\\!/; -var REGEX_LEADING_EXCAPED_HASH = /^\\#/; -var SLASH = '/'; -var KEY_IGNORE = typeof Symbol !== 'undefined' ? Symbol.for('node-ignore') -/* istanbul ignore next */ -: 'node-ignore'; - -var IgnoreBase = function () { - function IgnoreBase() { - _classCallCheck(this, IgnoreBase); - - this._rules = []; - this[KEY_IGNORE] = true; - this._initCache(); - } - - _createClass(IgnoreBase, [{ - key: '_initCache', - value: function _initCache() { - this._cache = {}; - } - - // @param {Array.|string|Ignore} pattern - - }, { - key: 'add', - value: function add(pattern) { - this._added = false; - - if (typeof pattern === 'string') { - pattern = pattern.split(/\r?\n/g); - } - - make_array(pattern).forEach(this._addPattern, this); - - // Some rules have just added to the ignore, - // making the behavior changed. - if (this._added) { - this._initCache(); - } - - return this; - } - - // legacy - - }, { - key: 'addPattern', - value: function addPattern(pattern) { - return this.add(pattern); - } - }, { - key: '_addPattern', - value: function _addPattern(pattern) { - // #32 - if (pattern && pattern[KEY_IGNORE]) { - this._rules = this._rules.concat(pattern._rules); - this._added = true; - return; - } - - if (this._checkPattern(pattern)) { - var rule = this._createRule(pattern); - this._added = true; - this._rules.push(rule); - } - } - }, { - key: '_checkPattern', - value: function _checkPattern(pattern) { - // > A blank line matches no files, so it can serve as a separator for readability. - return pattern && typeof pattern === 'string' && !REGEX_BLANK_LINE.test(pattern) - - // > A line starting with # serves as a comment. - && pattern.indexOf('#') !== 0; - } - }, { - key: 'filter', - value: function filter(paths) { - var _this = this; - - return make_array(paths).filter(function (path) { - return _this._filter(path); - }); - } - }, { - key: 'createFilter', - value: function createFilter() { - var _this2 = this; - - return function (path) { - return _this2._filter(path); - }; - } - }, { - key: 'ignores', - value: function ignores(path) { - return !this._filter(path); - } - }, { - key: '_createRule', - value: function _createRule(pattern) { - var origin = pattern; - var negative = false; - - // > An optional prefix "!" which negates the pattern; - if (pattern.indexOf('!') === 0) { - negative = true; - pattern = pattern.substr(1); - } - - pattern = pattern - // > Put a backslash ("\") in front of the first "!" for patterns that begin with a literal "!", for example, `"\!important!.txt"`. - .replace(REGEX_LEADING_EXCAPED_EXCLAMATION, '!') - // > Put a backslash ("\") in front of the first hash for patterns that begin with a hash. - .replace(REGEX_LEADING_EXCAPED_HASH, '#'); - - var regex = make_regex(pattern, negative); - - return { - origin: origin, - pattern: pattern, - negative: negative, - regex: regex - }; - } - - // @returns `Boolean` true if the `path` is NOT ignored - - }, { - key: '_filter', - value: function _filter(path, slices) { - if (!path) { - return false; - } - - if (path in this._cache) { - return this._cache[path]; - } - - if (!slices) { - // path/to/a.js - // ['path', 'to', 'a.js'] - slices = path.split(SLASH); - } - - slices.pop(); - - return this._cache[path] = slices.length - // > It is not possible to re-include a file if a parent directory of that file is excluded. - // If the path contains a parent directory, check the parent first - ? this._filter(slices.join(SLASH) + SLASH, slices) && this._test(path) - - // Or only test the path - : this._test(path); - } - - // @returns {Boolean} true if a file is NOT ignored - - }, { - key: '_test', - value: function _test(path) { - // Explicitly define variable type by setting matched to `0` - var matched = 0; - - this._rules.forEach(function (rule) { - // if matched = true, then we only test negative rules - // if matched = false, then we test non-negative rules - if (!(matched ^ rule.negative)) { - matched = rule.negative ^ rule.regex.test(path); - } - }); - - return !matched; - } - }]); - - return IgnoreBase; -}(); - -// > If the pattern ends with a slash, -// > it is removed for the purpose of the following description, -// > but it would only find a match with a directory. -// > In other words, foo/ will match a directory foo and paths underneath it, -// > but will not match a regular file or a symbolic link foo -// > (this is consistent with the way how pathspec works in general in Git). -// '`foo/`' will not match regular file '`foo`' or symbolic link '`foo`' -// -> ignore-rules will not deal with it, because it costs extra `fs.stat` call -// you could use option `mark: true` with `glob` - -// '`foo/`' should not continue with the '`..`' - - -var DEFAULT_REPLACER_PREFIX = [ - -// > Trailing spaces are ignored unless they are quoted with backslash ("\") -[ -// (a\ ) -> (a ) -// (a ) -> (a) -// (a \ ) -> (a ) -/\\?\s+$/, function (match) { - return match.indexOf('\\') === 0 ? ' ' : ''; -}], - -// replace (\ ) with ' ' -[/\\\s/g, function () { - return ' '; -}], - -// Escape metacharacters -// which is written down by users but means special for regular expressions. - -// > There are 12 characters with special meanings: -// > - the backslash \, -// > - the caret ^, -// > - the dollar sign $, -// > - the period or dot ., -// > - the vertical bar or pipe symbol |, -// > - the question mark ?, -// > - the asterisk or star *, -// > - the plus sign +, -// > - the opening parenthesis (, -// > - the closing parenthesis ), -// > - and the opening square bracket [, -// > - the opening curly brace {, -// > These special characters are often called "metacharacters". -[/[\\\^$.|?*+()\[{]/g, function (match) { - return '\\' + match; -}], - -// leading slash -[ - -// > A leading slash matches the beginning of the pathname. -// > For example, "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". -// A leading slash matches the beginning of the pathname -/^\//, function () { - return '^'; -}], - -// replace special metacharacter slash after the leading slash -[/\//g, function () { - return '\\/'; -}], [ -// > A leading "**" followed by a slash means match in all directories. -// > For example, "**/foo" matches file or directory "foo" anywhere, -// > the same as pattern "foo". -// > "**/foo/bar" matches file or directory "bar" anywhere that is directly under directory "foo". -// Notice that the '*'s have been replaced as '\\*' -/^\^*\\\*\\\*\\\//, - -// '**/foo' <-> 'foo' -function () { - return '^(?:.*\\/)?'; -}]]; - -var DEFAULT_REPLACER_SUFFIX = [ -// starting -[ -// there will be no leading '/' (which has been replaced by section "leading slash") -// If starts with '**', adding a '^' to the regular expression also works -/^(?=[^\^])/, function () { - return !/\/(?!$)/.test(this) - // > If the pattern does not contain a slash /, Git treats it as a shell glob pattern - // Actually, if there is only a trailing slash, git also treats it as a shell glob pattern - ? '(?:^|\\/)' - - // > Otherwise, Git treats the pattern as a shell glob suitable for consumption by fnmatch(3) - : '^'; -}], - -// two globstars -[ -// Use lookahead assertions so that we could match more than one `'/**'` -/\\\/\\\*\\\*(?=\\\/|$)/g, - -// Zero, one or several directories -// should not use '*', or it will be replaced by the next replacer - -// Check if it is not the last `'/**'` -function (match, index, str) { - return index + 6 < str.length - - // case: /**/ - // > A slash followed by two consecutive asterisks then a slash matches zero or more directories. - // > For example, "a/**/b" matches "a/b", "a/x/b", "a/x/y/b" and so on. - // '/**/' - ? '(?:\\/[^\\/]+)*' +module.exports.sync = options => { + options = normalizeOptions(options); - // case: /** - // > A trailing `"/**"` matches everything inside. + const paths = fastGlob.sync('**/.gitignore', { + ignore: DEFAULT_IGNORE.concat(options.ignore), + cwd: options.cwd + }); - // #21: everything inside but it should not include the current folder - : '\\/.+'; -}], + const files = paths.map(file => getFileSync(file, options.cwd)); + const ignores = reduceIgnore(files); -// intermediate wildcards -[ -// Never replace escaped '*' -// ignore rule '\*' will match the path '*' - -// 'abc.*/' -> go -// 'abc.*' -> skip this rule -/(^|[^\\]+)\\\*(?=.+)/g, - -// '*.js' matches '.js' -// '*.js' doesn't match 'abc' -function (match, p1) { - return p1 + '[^\\/]*'; -}], - -// trailing wildcard -[/(\^|\\\/)?\\\*$/, function (match, p1) { - return (p1 - // '\^': - // '/*' does not match '' - // '/*' does not match everything - - // '\\\/': - // 'abc/*' does not match 'abc/' - ? p1 + '[^/]+' - - // 'a*' matches 'a' - // 'a*' matches 'aa' - : '[^/]*') + '(?=$|\\/$)'; -}], [ -// unescape -/\\\\\\/g, function () { - return '\\'; -}]]; - -var POSITIVE_REPLACERS = [].concat(DEFAULT_REPLACER_PREFIX, [ - -// 'f' -// matches -// - /f(end) -// - /f/ -// - (start)f(end) -// - (start)f/ -// doesn't match -// - oof -// - foo -// pseudo: -// -> (^|/)f(/|$) - -// ending -[ -// 'js' will not match 'js.' -// 'ab' will not match 'abc' -/(?:[^*\/])$/, - -// 'js*' will not match 'a.js' -// 'js/' will not match 'a.js' -// 'js' will match 'a.js' and 'a.js/' -function (match) { - return match + '(?=$|\\/)'; -}]], DEFAULT_REPLACER_SUFFIX); - -var NEGATIVE_REPLACERS = [].concat(DEFAULT_REPLACER_PREFIX, [ - -// #24, #38 -// The MISSING rule of [gitignore docs](https://git-scm.com/docs/gitignore) -// A negative pattern without a trailing wildcard should not -// re-include the things inside that directory. - -// eg: -// ['node_modules/*', '!node_modules'] -// should ignore `node_modules/a.js` -[/(?:[^*])$/, function (match) { - return match + '(?=$|\\/$)'; -}]], DEFAULT_REPLACER_SUFFIX); + return getIsIgnoredPredecate(ignores, options.cwd); +}; -// A simple cache, because an ignore rule only has only one certain meaning -var cache = {}; -// @param {pattern} -function make_regex(pattern, negative) { - var r = cache[pattern]; - if (r) { - return r; - } +/***/ }), +/* 811 */ +/***/ (function(module, exports, __webpack_require__) { - var replacers = negative ? NEGATIVE_REPLACERS : POSITIVE_REPLACERS; +"use strict"; - var source = replacers.reduce(function (prev, current) { - return prev.replace(current[0], current[1].bind(pattern)); - }, pattern); +const {Transform} = __webpack_require__(138); - return cache[pattern] = new RegExp(source, 'i'); +class ObjectTransform extends Transform { + constructor() { + super({ + objectMode: true + }); + } } -// Windows -// -------------------------------------------------------------- -/* istanbul ignore if */ -if ( -// Detect `process` so that it can run in browsers. -typeof process !== 'undefined' && (process.env && process.env.IGNORE_TEST_WIN32 || process.platform === 'win32')) { +class FilterStream extends ObjectTransform { + constructor(filter) { + super(); + this._filter = filter; + } - var filter = IgnoreBase.prototype._filter; - var make_posix = function make_posix(str) { - return (/^\\\\\?\\/.test(str) || /[^\x00-\x80]+/.test(str) ? str : str.replace(/\\/g, '/') - ); - }; + _transform(data, encoding, callback) { + if (this._filter(data)) { + this.push(data); + } - IgnoreBase.prototype._filter = function (path, slices) { - path = make_posix(path); - return filter.call(this, path, slices); - }; + callback(); + } } +class UniqueStream extends ObjectTransform { + constructor() { + super(); + this._pushed = new Set(); + } -/***/ }), -/* 782 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -module.exports = function (str) { - var isExtendedLengthPath = /^\\\\\?\\/.test(str); - var hasNonAscii = /[^\x00-\x80]+/.test(str); + _transform(data, encoding, callback) { + if (!this._pushed.has(data)) { + this.push(data); + this._pushed.add(data); + } - if (isExtendedLengthPath || hasNonAscii) { - return str; + callback(); } +} - return str.replace(/\\/g, '/'); +module.exports = { + FilterStream, + UniqueStream }; /***/ }), -/* 783 */ +/* 812 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 0fa79fff6e0d9..050aadd402d8a 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -9,7 +9,7 @@ }, "scripts": { "build": "../../node_modules/.bin/webpack", - "kbn:watch": "../../node_modules/.bin/webpack --watch --progress", + "kbn:watch": "../../node_modules/.bin/webpack --watch", "prettier": "../../node_modules/.bin/prettier --write './src/**/*.ts'" }, "devDependencies": { diff --git a/packages/kbn-pm/src/cli.ts b/packages/kbn-pm/src/cli.ts index 6d033b4121d99..f6ea4d7124ab2 100644 --- a/packages/kbn-pm/src/cli.ts +++ b/packages/kbn-pm/src/cli.ts @@ -75,7 +75,7 @@ export async function run(argv: string[]) { }, default: { cache: true, - 'force-install': true, + 'force-install': false, offline: false, validate: true, }, diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 4a6a43ff2d91f..b383a52be63f5 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -17,7 +17,12 @@ import { getAllChecksums } from '../utils/project_checksums'; import { BootstrapCacheFile } from '../utils/bootstrap_cache_file'; import { readYarnLock } from '../utils/yarn_lock'; import { validateDependencies } from '../utils/validate_dependencies'; -import { ensureYarnIntegrityFileExists, installBazelTools, runBazel } from '../utils/bazel'; +import { + ensureYarnIntegrityFileExists, + installBazelTools, + runBazel, + yarnIntegrityFileExists, +} from '../utils/bazel'; export const BootstrapCommand: ICommand = { description: 'Install dependencies and crosslink projects', @@ -33,7 +38,13 @@ export const BootstrapCommand: ICommand = { const batchedNonBazelProjects = topologicallyBatchProjects(nonBazelProjectsOnly, projectGraph); const kibanaProjectPath = projects.get('kibana')?.path || ''; const runOffline = options?.offline === true; - const forceInstall = !!options && options['force-install'] === true; + + // Force install is set in case a flag is passed or + // if the `.yarn-integrity` file is not found which + // will be indicated by the return of yarnIntegrityFileExists. + const forceInstall = + (!!options && options['force-install'] === true) || + !(await yarnIntegrityFileExists(resolve(kibanaProjectPath, 'node_modules'))); // Ensure we have a `node_modules/.yarn-integrity` file as we depend on it // for bazel to know it has to re-install the node_modules after a reset or a clean @@ -51,9 +62,6 @@ export const BootstrapCommand: ICommand = { // That way non bazel projects could depend on bazel projects but not the other way around // That is only intended during the migration process while non Bazel projects are not removed at all. // - // Until we have our first package build within Bazel we will always need to directly call the yarn rule - // otherwise yarn install won't trigger as we don't have any npm dependency within Bazel - // TODO: Change CLI default in order to not force install as soon as we have our first Bazel package being built if (forceInstall) { await runBazel(['run', '@nodejs//:yarn'], runOffline); } diff --git a/packages/kbn-pm/src/production/build_bazel_production_projects.ts b/packages/kbn-pm/src/production/build_bazel_production_projects.ts index 313622d44276a..07c0b651f5ad1 100644 --- a/packages/kbn-pm/src/production/build_bazel_production_projects.ts +++ b/packages/kbn-pm/src/production/build_bazel_production_projects.ts @@ -37,7 +37,7 @@ export async function buildBazelProductionProjects({ log.info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); await runBazel(['build', '//packages:build']); - log.info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); + log.info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete`); for (const project of projects.values()) { await copyToBuild(project, kibanaRoot, buildRoot); @@ -62,7 +62,7 @@ async function copyToBuild(project: Project, kibanaRoot: string, buildRoot: stri const buildProjectPath = resolve(buildRoot, relativeProjectPath); await copy(['**/*'], buildProjectPath, { - cwd: join(kibanaRoot, 'bazel', 'bin', 'packages', basename(buildProjectPath), 'npm_module'), + cwd: join(kibanaRoot, 'bazel-bin', 'packages', basename(buildProjectPath), 'npm_module'), dot: true, onlyFiles: true, parents: true, @@ -88,12 +88,12 @@ async function applyCorrectPermissions(project: Project, kibanaRoot: string, bui const buildProjectPath = resolve(buildRoot, relativeProjectPath); const allPluginPaths = await globby([`**/*`], { onlyFiles: false, - cwd: join(kibanaRoot, 'bazel', 'bin', 'packages', basename(buildProjectPath), 'npm_module'), + cwd: buildProjectPath, dot: true, }); for (const pluginPath of allPluginPaths) { - const resolvedPluginPath = resolve(buildRoot, pluginPath); + const resolvedPluginPath = resolve(buildProjectPath, pluginPath); if (await isFile(resolvedPluginPath)) { await chmod(resolvedPluginPath, 0o644); } diff --git a/packages/kbn-pm/src/utils/__snapshots__/link_project_executables.test.ts.snap b/packages/kbn-pm/src/utils/__snapshots__/link_project_executables.test.ts.snap index c037c2a4976b4..8aeae04c265cf 100644 --- a/packages/kbn-pm/src/utils/__snapshots__/link_project_executables.test.ts.snap +++ b/packages/kbn-pm/src/utils/__snapshots__/link_project_executables.test.ts.snap @@ -11,6 +11,7 @@ Object { "mkdirp": Array [], "readFile": Array [], "rmdirp": Array [], + "tryRealpath": Array [], "unlink": Array [], "writeFile": Array [], } @@ -27,6 +28,7 @@ Object { "mkdirp": Array [], "readFile": Array [], "rmdirp": Array [], + "tryRealpath": Array [], "unlink": Array [], "writeFile": Array [], } diff --git a/packages/kbn-pm/src/utils/bazel/index.ts b/packages/kbn-pm/src/utils/bazel/index.ts index 0b755ba2446a0..a3651039161b8 100644 --- a/packages/kbn-pm/src/utils/bazel/index.ts +++ b/packages/kbn-pm/src/utils/bazel/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export * from './ensure_yarn_integrity_exists'; +export * from './yarn_integrity'; export * from './get_cache_folders'; export * from './install_tools'; export * from './run'; diff --git a/packages/kbn-pm/src/utils/bazel/yarn_integrity.ts b/packages/kbn-pm/src/utils/bazel/yarn_integrity.ts new file mode 100644 index 0000000000000..3a72f5ca080b8 --- /dev/null +++ b/packages/kbn-pm/src/utils/bazel/yarn_integrity.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { join } from 'path'; +import { isFile, mkdirp, tryRealpath, writeFile } from '../fs'; + +export async function yarnIntegrityFileExists(nodeModulesPath: string) { + try { + const nodeModulesRealPath = await tryRealpath(nodeModulesPath); + const yarnIntegrityFilePath = join(nodeModulesRealPath, '.yarn-integrity'); + + // check if the file already exists + if (await isFile(yarnIntegrityFilePath)) { + return true; + } + } catch { + // no-op + } + + return false; +} + +export async function ensureYarnIntegrityFileExists(nodeModulesPath: string) { + try { + const nodeModulesRealPath = await tryRealpath(nodeModulesPath); + const yarnIntegrityFilePath = join(nodeModulesRealPath, '.yarn-integrity'); + + // ensure node_modules folder is created + await mkdirp(nodeModulesRealPath); + + // write a blank file in case it doesn't exists + await writeFile(yarnIntegrityFilePath, '', { flag: 'wx' }); + } catch { + // no-op + } +} diff --git a/packages/kbn-pm/src/utils/fs.ts b/packages/kbn-pm/src/utils/fs.ts index dd961b8321446..5739d319e08e7 100644 --- a/packages/kbn-pm/src/utils/fs.ts +++ b/packages/kbn-pm/src/utils/fs.ts @@ -20,6 +20,7 @@ const symlink = promisify(fs.symlink); export const chmod = promisify(fs.chmod); const cmdShim = promisify(cmdShimCb); const mkdir = promisify(fs.mkdir); +const realpathNative = promisify(fs.realpath.native); export const mkdirp = async (path: string) => await mkdir(path, { recursive: true }); export const rmdirp = async (path: string) => await del(path, { force: true }); export const unlink = promisify(fs.unlink); @@ -96,3 +97,17 @@ async function forceCreate(src: string, dest: string, type: string) { await symlink(src, dest, type); } + +export async function tryRealpath(path: string): Promise { + let calculatedPath = path; + + try { + calculatedPath = await realpathNative(path); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + return calculatedPath; +} diff --git a/packages/kbn-pm/src/utils/package_json.ts b/packages/kbn-pm/src/utils/package_json.ts index b405b544ab800..e635c2566e65a 100644 --- a/packages/kbn-pm/src/utils/package_json.ts +++ b/packages/kbn-pm/src/utils/package_json.ts @@ -35,7 +35,7 @@ export const createProductionPackageJson = (pkgJson: IPackageJson) => ({ export const isLinkDependency = (depVersion: string) => depVersion.startsWith('link:'); export const isBazelPackageDependency = (depVersion: string) => - depVersion.startsWith('link:bazel/bin/'); + depVersion.startsWith('link:bazel-bin/'); /** * Replaces `link:` dependencies with `file:` dependencies. When installing @@ -46,7 +46,7 @@ export const isBazelPackageDependency = (depVersion: string) => * will then _copy_ the `file:` dependencies into `node_modules` instead of * symlinking like we do in development. * - * Additionally it also taken care of replacing `link:bazel/bin/` with + * Additionally it also taken care of replacing `link:bazel-bin/` with * `file:` so we can also support the copy of the Bazel packages dist already into * build/packages to be copied into the node_modules */ @@ -61,7 +61,7 @@ export function transformDependencies(dependencies: IPackageDependencies = {}) { } if (isBazelPackageDependency(depVersion)) { - newDeps[name] = depVersion.replace('link:bazel/bin/', 'file:'); + newDeps[name] = depVersion.replace('link:bazel-bin/', 'file:').replace('/npm_module', ''); continue; } diff --git a/packages/kbn-pm/src/utils/project.ts b/packages/kbn-pm/src/utils/project.ts index 797a9a36df78f..5d2a0547b2577 100644 --- a/packages/kbn-pm/src/utils/project.ts +++ b/packages/kbn-pm/src/utils/project.ts @@ -92,7 +92,10 @@ export class Project { public ensureValidProjectDependency(project: Project) { const relativePathToProject = normalizePath(Path.relative(this.path, project.path)); const relativePathToProjectIfBazelPkg = normalizePath( - Path.relative(this.path, `bazel/bin/packages/${Path.basename(project.path)}`) + Path.relative( + this.path, + `${__dirname}/../../../bazel-bin/packages/${Path.basename(project.path)}/npm_module` + ) ); const versionInPackageJson = this.allDependencies[project.name]; @@ -100,7 +103,7 @@ export class Project { const expectedVersionInPackageJsonIfBazelPkg = `link:${relativePathToProjectIfBazelPkg}`; // TODO: after introduce bazel to build all the packages and completely remove the support for kbn packages - // do not allow child projects to hold dependencies + // do not allow child projects to hold dependencies, unless they are meant to be published externally if ( versionInPackageJson === expectedVersionInPackageJson || versionInPackageJson === expectedVersionInPackageJsonIfBazelPkg diff --git a/packages/kbn-pm/tsconfig.json b/packages/kbn-pm/tsconfig.json index 175c4701f2e5b..558cff6556ff6 100644 --- a/packages/kbn-pm/tsconfig.json +++ b/packages/kbn-pm/tsconfig.json @@ -1,16 +1,14 @@ { "extends": "../../tsconfig.base.json", - "include": [ - "./index.d.ts", - "./src/**/*.ts", - "./dist/*.d.ts" - ], - "exclude": [], "compilerOptions": { "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-pm", "types": [ "jest", "node" ] - } + }, + "include": [ + "./index.d.ts", + "./src/**/*.ts" + ] } diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json index a8f99689f3335..6c65a0dd6e475 100644 --- a/packages/kbn-server-http-tools/package.json +++ b/packages/kbn-server-http-tools/package.json @@ -1,6 +1,7 @@ { "name": "@kbn/server-http-tools", "main": "./target/index.js", + "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true, diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index ec84b963aed70..2f3e4626a04ce 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -1,14 +1,14 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "target", + "incremental": false, + "outDir": "./target", "declaration": true, - "declarationMap": true + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-server-http-tools/src" }, "include": [ "src/**/*" - ], - "dependencies": { - "@kbn/std": "link:../kbn-std" - } + ] } diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json index fd186a6e43d1c..d2ed46dcad6f8 100644 --- a/packages/kbn-std/tsconfig.json +++ b/packages/kbn-std/tsconfig.json @@ -1,13 +1,23 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, + "incremental": false, "declarationDir": "./target", "outDir": "./target", "stripInternal": true, + "declaration": true, "declarationMap": true, - "types": ["jest", "node"] + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-std/src", + "types": [ + "jest", + "node" + ] }, - "include": ["./src/**/*.ts"], - "exclude": ["target"] + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "**/__fixture__/**/*" + ] } diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 75801948bb20b..fdc7359aab58d 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -4,6 +4,7 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "kibana": { "devOnly": true }, diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 814a3963c9f49..db10d4630ff9c 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,9 +1,19 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "declaration": true, + "incremental": false, "outDir": "target", - "skipLibCheck": true + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-storybook", + "types": [ + "node" + ] }, - "include": ["*.ts", "lib/*.ts"] + "include": [ + "*.ts", + "lib/*.ts" + ] } diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 28d67c73eb49e..2ae1f596a1c68 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/index.d.ts", "private": true, "kibana": { "devOnly": true diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 98512053a5c92..39946fe9907e5 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,7 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-telemetry-tools" + "incremental": false, + "outDir": "./target", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-telemetry-tools/src" }, "include": [ "src/**/*", diff --git a/packages/kbn-test/index.d.ts b/packages/kbn-test/index.d.ts deleted file mode 100644 index 004ac67f4b0c4..0000000000000 --- a/packages/kbn-test/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './src/index'; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 4949d6d1f9fad..225f93d487823 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -107,4 +107,7 @@ module.exports = { '!**/*.d.ts', '!**/index.{js,ts}', ], + + // A custom resolver to preserve symlinks by default + resolver: '/packages/kbn-test/target/jest/setup/preserve_symlinks_resolver.js', }; diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 0f0ba8d79a1c1..a2dc8f84cfb51 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -4,6 +4,7 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", + "types": "./target/types/index.d.ts", "scripts": { "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --source-maps", diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts similarity index 100% rename from packages/kbn-test/src/functional_test_runner/fake_mocha_types.d.ts rename to packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index eef9e833fe5a8..1cb1e58a265d5 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -10,6 +10,7 @@ export { Lifecycle } from './lifecycle'; export { LifecyclePhase } from './lifecycle_phase'; export { readConfigFile, Config } from './config'; export { readProviderSpec, ProviderCollection } from './providers'; +// @internal export { runTests, setupMocha } from './mocha'; export { FailureMetadata } from './failure_metadata'; export * from './docker_servers'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts index de55df34fa88b..4f27980db61d1 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/index.ts @@ -7,5 +7,7 @@ */ // @ts-ignore will be replaced shortly +// @internal export { setupMocha } from './setup_mocha'; +// @internal export { runTests } from './run_tests'; diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 919dc8b4477f3..ef167bc5d7819 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// @internal import { runTestsCli, processRunTestsCliOptions, @@ -14,27 +15,34 @@ import { // @ts-ignore not typed yet } from './functional_tests/cli'; +// @internal export { runTestsCli, processRunTestsCliOptions, startServersCli, processStartServersCliOptions }; // @ts-ignore not typed yet +// @internal export { runTests, startServers } from './functional_tests/tasks'; // @ts-ignore not typed yet +// @internal export { KIBANA_ROOT } from './functional_tests/lib/paths'; // @ts-ignore not typed yet +// @internal export { esTestConfig, createLegacyEsTestCluster } from './legacy_es'; // @ts-ignore not typed yet +// @internal export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn'; // @ts-ignore not typed yet +// @internal export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth'; export { readConfigFile } from './functional_test_runner/lib/config/read_config_file'; export { runFtrCli } from './functional_test_runner/cli'; +// @internal export { setupJUnitReportGeneration, escapeCdata } from './mocha'; export { runFailedTestsReporterCli } from './failed_tests_reporter'; diff --git a/packages/kbn-test/src/jest/setup/preserve_symlinks_resolver.js b/packages/kbn-test/src/jest/setup/preserve_symlinks_resolver.js new file mode 100644 index 0000000000000..711bf2c9aa189 --- /dev/null +++ b/packages/kbn-test/src/jest/setup/preserve_symlinks_resolver.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Inspired in a discussion found at https://github.com/facebook/jest/issues/5356 as Jest currently doesn't +// offer any other option to preserve symlinks. +// +// It would be available once https://github.com/facebook/jest/pull/9976 got merged. + +const resolve = require('resolve'); + +module.exports = (request, options) => { + try { + return resolve.sync(request, { + basedir: options.basedir, + extensions: options.extensions, + preserveSymlinks: true, + }); + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + return options.defaultResolver(request, options); + } + + throw error; + } +}; diff --git a/packages/kbn-test/src/kbn_archiver_cli.ts b/packages/kbn-test/src/kbn_archiver_cli.ts index 98bfa6eaa4046..04581a8354668 100644 --- a/packages/kbn-test/src/kbn_archiver_cli.ts +++ b/packages/kbn-test/src/kbn_archiver_cli.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Url from 'url'; import { RunWithCommands, createFlagError, Flags } from '@kbn/dev-utils'; -import { KbnClient } from '@kbn/test'; +import { KbnClient } from './kbn_client'; import { readConfigFile } from './functional_test_runner'; diff --git a/packages/kbn-test/src/mocha/index.ts b/packages/kbn-test/src/mocha/index.ts index 1cff5202f33b9..4ada51c7ae013 100644 --- a/packages/kbn-test/src/mocha/index.ts +++ b/packages/kbn-test/src/mocha/index.ts @@ -7,8 +7,11 @@ */ // @ts-ignore not typed yet +// @internal export { setupJUnitReportGeneration } from './junit_report_generation'; // @ts-ignore not typed yet +// @internal export { recordLog, snapshotLogsForRunnable } from './log_cache'; // @ts-ignore not typed yet +// @internal export { escapeCdata } from './xml'; diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 6d94389f82caa..8536ad7e0c12f 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,22 +1,26 @@ { "extends": "../../tsconfig.base.json", - "include": [ - "types/**/*", - "src/**/*", - "index.d.ts" - ], - "exclude": [ - "types/ftr_globals/**/*" - ], "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, + "incremental": false, "outDir": "./target/types", + "stripInternal": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-test/src", "types": [ "jest", "node" ], - "stripInternal": true, - "declarationMap": true - } + }, + "include": [ + "types/**/*", + "src/**/*", + "index.d.ts" + ], + "exclude": [ + "types/ftr_globals/**/*", + "**/__fixtures__/**/*" + ] } diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index ede617908fd3d..f14c793d22a09 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -47,3 +47,5 @@ export const LodashFp = require('lodash/fp'); // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); export const KbnAnalytics = require('@kbn/analytics'); +export const KbnStd = require('@kbn/std'); +export const SaferLodashSet = require('@elastic/safer-lodash-set'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index d1217dd8db0d4..0542bc89ff9e4 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -58,5 +58,7 @@ exports.externals = { */ tslib: '__kbnSharedDeps__.TsLib', '@kbn/analytics': '__kbnSharedDeps__.KbnAnalytics', + '@kbn/std': '__kbnSharedDeps__.KbnStd', + '@elastic/safer-lodash-set': '__kbnSharedDeps__.SaferLodashSet', }; exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 135884fbf13e7..76e6843bea2f8 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -177,22 +177,22 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ compiler.hooks.emit.tap('MetricsPlugin', (compilation) => { const metrics = [ { - group: '@kbn/ui-shared-deps asset size', - id: 'kbn-ui-shared-deps.js', + group: 'page load bundle size', + id: 'kbnUiSharedDeps-js', value: compilation.assets['kbn-ui-shared-deps.js'].size(), }, { - group: '@kbn/ui-shared-deps asset size', - id: 'kbn-ui-shared-deps.@elastic.js', - value: compilation.assets['kbn-ui-shared-deps.@elastic.js'].size(), - }, - { - group: '@kbn/ui-shared-deps asset size', - id: 'css', + group: 'page load bundle size', + id: 'kbnUiSharedDeps-css', value: compilation.assets['kbn-ui-shared-deps.css'].size() + compilation.assets['kbn-ui-shared-deps.v7.light.css'].size(), }, + { + group: 'page load bundle size', + id: 'kbnUiSharedDeps-elastic', + value: compilation.assets['kbn-ui-shared-deps.@elastic.js'].size(), + }, ]; compilation.emitAsset( diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index 33419ee0f1ec4..ad7dcc6b906c3 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "target", + "main": "target/index.js", "types": "target/index.d.ts", "kibana": { "devOnly": false diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index c2d206526e6f4..cfa782e5d38d2 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -1,18 +1,22 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "declaration": true, - "declarationDir": "./target", + "incremental": false, "outDir": "./target", + "declarationDir": "./target", "stripInternal": true, + "declaration": true, "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-utility-types", "types": [ - "node", - "jest" + "jest", + "node" ] }, - "include": ["index.ts", "jest/**/*", "test-d/**/*"], - "exclude": [ - "target" + "include": [ + "index.ts", + "jest/**/*", + "test-d/**/*" ] } diff --git a/packages/kbn-utils/package.json b/packages/kbn-utils/package.json index 902eef82736fe..b6bb7759c40ef 100644 --- a/packages/kbn-utils/package.json +++ b/packages/kbn-utils/package.json @@ -1,6 +1,7 @@ { "name": "@kbn/utils", "main": "./target/index.js", + "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true, diff --git a/packages/kbn-utils/src/package_json/index.ts b/packages/kbn-utils/src/package_json/index.ts index 40ce353780749..d9304cee2ca38 100644 --- a/packages/kbn-utils/src/package_json/index.ts +++ b/packages/kbn-utils/src/package_json/index.ts @@ -14,3 +14,7 @@ export const kibanaPackageJson = { __dirname: dirname(resolve(REPO_ROOT, 'package.json')), ...require(resolve(REPO_ROOT, 'package.json')), }; + +export const isKibanaDistributable = () => { + return kibanaPackageJson.build && kibanaPackageJson.build.distributable === true; +}; diff --git a/packages/kbn-utils/tsconfig.json b/packages/kbn-utils/tsconfig.json index e9dd6313e6f79..e6c83767c30dc 100644 --- a/packages/kbn-utils/tsconfig.json +++ b/packages/kbn-utils/tsconfig.json @@ -1,9 +1,16 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "incremental": false, "outDir": "target", "declaration": true, - "declarationMap": true + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-utils/src", + "types": [ + "jest", + "node" + ] }, "include": [ "src/**/*" diff --git a/scripts/build_kibana_platform_plugins.js b/scripts/build_kibana_platform_plugins.js index fa630e0bb1808..9038d08364400 100644 --- a/scripts/build_kibana_platform_plugins.js +++ b/scripts/build_kibana_platform_plugins.js @@ -7,6 +7,7 @@ */ require('../src/setup_node_env/ensure_node_preserve_symlinks'); +require('source-map-support/register'); require('@kbn/optimizer').runKbnOptimizerCli({ defaultLimitsPath: require.resolve('../packages/kbn-optimizer/limits.yml'), }); diff --git a/src/cli/cli.js b/src/cli/cli.js index 4540bf4a3f93c..d3bff4f492a80 100644 --- a/src/cli/cli.js +++ b/src/cli/cli.js @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Command from './command'; import serveCommand from './serve/serve'; diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 86b4ac53841f7..ad83965efde33 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -12,8 +12,7 @@ import { statSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; -import { getConfigPath, fromRoot } from '@kbn/utils'; -import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; +import { getConfigPath, fromRoot, isKibanaDistributable } from '@kbn/utils'; import { readKeystore } from '../keystore/read_keystore'; function canRequire(path) { @@ -65,9 +64,10 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { delete rawConfig.xpack; } - if (opts.dev) { - set('env', 'development'); + // only used to set cliArgs.envName, we don't want to inject that into the config + delete extraCliOptions.env; + if (opts.dev) { if (!has('elasticsearch.username')) { set('elasticsearch.username', 'kibana_system'); } @@ -184,7 +184,7 @@ export default function (program) { .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) .option('--optimize', 'Deprecated, running the optimizer is no longer required'); - if (!IS_KIBANA_DISTRIBUTABLE) { + if (!isKibanaDistributable()) { command .option('--oss', 'Start Kibana without X-Pack') .option( @@ -228,6 +228,7 @@ export default function (program) { // no longer supported quiet: !!opts.quiet, silent: !!opts.silent, + verbose: !!opts.verbose, watch: !!opts.watch, runExamples: !!opts.runExamples, // We want to run without base path when the `--run-examples` flag is given so that we can use local diff --git a/src/cli_encryption_keys/cli_encryption_keys.js b/src/cli_encryption_keys/cli_encryption_keys.js index e922b9354d291..acee81aabb706 100644 --- a/src/cli_encryption_keys/cli_encryption_keys.js +++ b/src/cli_encryption_keys/cli_encryption_keys.js @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; + import Command from '../cli/command'; import { EncryptionConfig } from './encryption_config'; diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index b325f685766aa..9f44e5d56e9d2 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -7,8 +7,8 @@ */ import _ from 'lodash'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; -import { pkg } from '../core/server/utils'; import Command from '../cli/command'; import { Keystore } from '../cli/keystore'; diff --git a/src/cli_plugin/cli.js b/src/cli_plugin/cli.js index 24ccba6a23397..5ef142192c509 100644 --- a/src/cli_plugin/cli.js +++ b/src/cli_plugin/cli.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { pkg } from '../core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import Command from '../cli/command'; import { listCommand } from './list'; import { installCommand } from './install'; diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index c028facc28e2b..2683dd41d2bb3 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { getConfigPath } from '@kbn/utils'; -import { pkg } from '../../core/server/utils'; +import { getConfigPath, kibanaPackageJson as pkg } from '@kbn/utils'; import { install } from './install'; import { Logger } from '../lib/logger'; import { parse, parseMilliseconds } from './settings'; diff --git a/src/cli_plugin/install/kibana.js b/src/cli_plugin/install/kibana.js index 29cb8df7401b6..1de157b951d03 100644 --- a/src/cli_plugin/install/kibana.js +++ b/src/cli_plugin/install/kibana.js @@ -9,7 +9,7 @@ import path from 'path'; import { statSync } from 'fs'; -import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; +import { versionSatisfies, cleanVersion } from './utils/version'; export function existingInstall(settings, logger) { try { diff --git a/src/cli_plugin/install/settings.js b/src/cli_plugin/install/settings.js index 94473cc12aab2..e1536d66e0529 100644 --- a/src/cli_plugin/install/settings.js +++ b/src/cli_plugin/install/settings.js @@ -7,10 +7,8 @@ */ import { resolve } from 'path'; - import expiry from 'expiry-js'; - -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; function generateUrls({ version, plugin }) { return [ diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index f06fd7eca7902..c7985763524ed 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -7,8 +7,8 @@ */ import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { fromRoot } from '@kbn/utils'; -import { fromRoot } from '../../core/server/utils'; import { parseMilliseconds, parse } from './settings'; const SECOND = 1000; diff --git a/src/legacy/utils/version.js b/src/cli_plugin/install/utils/version.js similarity index 100% rename from src/legacy/utils/version.js rename to src/cli_plugin/install/utils/version.js diff --git a/src/cli_plugin/list/index.js b/src/cli_plugin/list/index.js index ce55b939b8a4c..02d1ed19f8445 100644 --- a/src/cli_plugin/list/index.js +++ b/src/cli_plugin/list/index.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; import { list } from './list'; import { Logger } from '../lib/logger'; import { logWarnings } from '../lib/log_warnings'; diff --git a/src/cli_plugin/remove/settings.js b/src/cli_plugin/remove/settings.js index 333fa7cb0f2e1..2381770ee0a65 100644 --- a/src/cli_plugin/remove/settings.js +++ b/src/cli_plugin/remove/settings.js @@ -7,8 +7,7 @@ */ import { resolve } from 'path'; - -import { fromRoot } from '../../core/server/utils'; +import { fromRoot } from '@kbn/utils'; export function parse(command, options) { const settings = { diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 278bbe469e862..b68a7ced118d2 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -28,6 +28,7 @@ import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; import { IntegrationsService } from './integrations'; +import { DeprecationsService } from './deprecations'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; @@ -82,7 +83,7 @@ export class CoreSystem { private readonly rendering: RenderingService; private readonly integrations: IntegrationsService; private readonly coreApp: CoreApp; - + private readonly deprecations: DeprecationsService; private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; private fatalErrorsSetup: FatalErrorsSetup | null = null; @@ -113,6 +114,7 @@ export class CoreSystem { this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); + this.deprecations = new DeprecationsService(); this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); @@ -195,6 +197,7 @@ export class CoreSystem { injectedMetadata, notifications, }); + const deprecations = this.deprecations.start({ http }); this.coreApp.start({ application, http, notifications, uiSettings }); @@ -210,6 +213,7 @@ export class CoreSystem { overlays, uiSettings, fatalErrors, + deprecations, }; await this.plugins.start(core); @@ -252,6 +256,7 @@ export class CoreSystem { this.chrome.stop(); this.i18n.stop(); this.application.stop(); + this.deprecations.stop(); this.rootDomElement.textContent = ''; } } diff --git a/src/core/public/deprecations/deprecations_client.test.ts b/src/core/public/deprecations/deprecations_client.test.ts new file mode 100644 index 0000000000000..2f52f7b4af195 --- /dev/null +++ b/src/core/public/deprecations/deprecations_client.test.ts @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { httpServiceMock } from '../http/http_service.mock'; +import { DeprecationsClient } from './deprecations_client'; +import type { DomainDeprecationDetails } from '../../server/types'; + +describe('DeprecationsClient', () => { + const http = httpServiceMock.createSetupContract(); + const mockDeprecations = [ + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-2' }, + ]; + + beforeEach(() => { + http.fetch.mockReset(); + http.fetch.mockResolvedValue({ deprecations: mockDeprecations }); + }); + + describe('getAllDeprecations', () => { + it('returns a list of deprecations', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const deprecations = await deprecationsClient.getAllDeprecations(); + expect(http.fetch).toBeCalledTimes(1); + expect(http.fetch).toBeCalledWith('/api/deprecations/', { + asSystemRequest: true, + }); + + expect(deprecations).toEqual(mockDeprecations); + }); + }); + + describe('getDeprecations', () => { + it('returns deprecations for a single domainId', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const deprecations = await deprecationsClient.getDeprecations('testPluginId-1'); + + expect(deprecations.length).toBe(2); + expect(deprecations).toEqual([ + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-1' }, + ]); + }); + + it('returns [] if the domainId does not have any deprecations', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const deprecations = await deprecationsClient.getDeprecations('testPluginId-4'); + + expect(deprecations).toEqual([]); + }); + + it('calls the fetch api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + http.fetch.mockResolvedValueOnce({ + deprecations: [{ domainId: 'testPluginId-1' }, { domainId: 'testPluginId-1' }], + }); + http.fetch.mockResolvedValueOnce({ + deprecations: [{ domainId: 'testPluginId-2' }, { domainId: 'testPluginId-2' }], + }); + const results = [ + ...(await deprecationsClient.getDeprecations('testPluginId-1')), + ...(await deprecationsClient.getDeprecations('testPluginId-2')), + ]; + + expect(http.fetch).toBeCalledTimes(2); + expect(results).toEqual([ + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-1' }, + { domainId: 'testPluginId-2' }, + { domainId: 'testPluginId-2' }, + ]); + }); + }); + + describe('isDeprecationResolvable', () => { + it('returns true if deprecation has correctiveActions.api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + }, + }, + }; + + const isResolvable = deprecationsClient.isDeprecationResolvable(mockDeprecationDetails); + + expect(isResolvable).toBe(true); + }); + + it('returns false if deprecation is missing correctiveActions.api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: {}, + }; + + const isResolvable = deprecationsClient.isDeprecationResolvable(mockDeprecationDetails); + + expect(isResolvable).toBe(false); + }); + }); + + describe('resolveDeprecation', () => { + it('fails if deprecation is not resolvable', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: {}, + }; + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(result).toEqual({ + status: 'fail', + reason: 'deprecation has no correctiveAction via api.', + }); + }); + + it('fetches the deprecation api', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + }, + }, + }; + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(http.fetch).toBeCalledTimes(1); + expect(http.fetch).toBeCalledWith({ + path: 'some-path', + method: 'POST', + asSystemRequest: true, + body: JSON.stringify({ + extra_param: 123, + deprecationDetails: { domainId: 'testPluginId-1' }, + }), + }); + expect(result).toEqual({ status: 'ok' }); + }); + + it('fails when fetch fails', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockResponse = 'Failed to fetch'; + const mockDeprecationDetails: DomainDeprecationDetails = { + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + }, + }, + }; + http.fetch.mockRejectedValue({ body: { message: mockResponse } }); + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(result).toEqual({ status: 'fail', reason: mockResponse }); + }); + }); +}); diff --git a/src/core/public/deprecations/deprecations_client.ts b/src/core/public/deprecations/deprecations_client.ts new file mode 100644 index 0000000000000..e510ab1e79d17 --- /dev/null +++ b/src/core/public/deprecations/deprecations_client.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 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 { HttpStart } from '../http'; +import type { DomainDeprecationDetails, DeprecationsGetResponse } from '../../server/types'; + +/* @internal */ +export interface DeprecationsClientDeps { + http: Pick; +} + +/* @internal */ +export type ResolveDeprecationResponse = { status: 'ok' } | { status: 'fail'; reason: string }; + +export class DeprecationsClient { + private readonly http: Pick; + constructor({ http }: DeprecationsClientDeps) { + this.http = http; + } + + private fetchDeprecations = async (): Promise => { + const { deprecations } = await this.http.fetch('/api/deprecations/', { + asSystemRequest: true, + }); + + return deprecations; + }; + + public getAllDeprecations = async () => { + return await this.fetchDeprecations(); + }; + + public getDeprecations = async (domainId: string) => { + const deprecations = await this.fetchDeprecations(); + return deprecations.filter((deprecation) => deprecation.domainId === domainId); + }; + + public isDeprecationResolvable = (details: DomainDeprecationDetails) => { + return typeof details.correctiveActions.api === 'object'; + }; + + public resolveDeprecation = async ( + details: DomainDeprecationDetails + ): Promise => { + const { domainId, correctiveActions } = details; + // explicit check required for TS type guard + if (typeof correctiveActions.api !== 'object') { + return { + status: 'fail', + reason: 'deprecation has no correctiveAction via api.', + }; + } + + const { body, method, path } = correctiveActions.api; + try { + await this.http.fetch({ + path, + method, + asSystemRequest: true, + body: JSON.stringify({ + ...body, + deprecationDetails: { domainId }, + }), + }); + return { status: 'ok' }; + } catch (err) { + return { + status: 'fail', + reason: err.body.message, + }; + } + }; +} diff --git a/src/core/public/deprecations/deprecations_service.mock.ts b/src/core/public/deprecations/deprecations_service.mock.ts new file mode 100644 index 0000000000000..5bcd52982d513 --- /dev/null +++ b/src/core/public/deprecations/deprecations_service.mock.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 type { PublicMethodsOf } from '@kbn/utility-types'; +import { DeprecationsService } from './deprecations_service'; +import type { DeprecationsServiceStart } from './deprecations_service'; + +const createServiceMock = (): jest.Mocked => ({ + getAllDeprecations: jest.fn().mockResolvedValue([]), + getDeprecations: jest.fn().mockResolvedValue([]), + isDeprecationResolvable: jest.fn().mockReturnValue(false), + resolveDeprecation: jest.fn().mockResolvedValue({ status: 'ok', payload: {} }), +}); + +const createMock = () => { + const mocked: jest.Mocked> = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockReturnValue(void 0); + mocked.start.mockReturnValue(createServiceMock()); + return mocked; +}; + +export const deprecationsServiceMock = { + create: createMock, + createSetupContract: () => void 0, + createStartContract: createServiceMock, +}; diff --git a/src/core/public/deprecations/deprecations_service.ts b/src/core/public/deprecations/deprecations_service.ts new file mode 100644 index 0000000000000..d06e0071d2bc7 --- /dev/null +++ b/src/core/public/deprecations/deprecations_service.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 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 { CoreService } from '../../types'; +import type { HttpStart } from '../http'; +import { DeprecationsClient, ResolveDeprecationResponse } from './deprecations_client'; +import type { DomainDeprecationDetails } from '../../server/types'; + +/** + * DeprecationsService provides methods to fetch domain deprecation details from + * the Kibana server. + * + * @public + */ +export interface DeprecationsServiceStart { + /** + * Grabs deprecations details for all domains. + */ + getAllDeprecations: () => Promise; + /** + * Grabs deprecations for a specific domain. + * + * @param {string} domainId + */ + getDeprecations: (domainId: string) => Promise; + /** + * Returns a boolean if the provided deprecation can be automatically resolvable. + * + * @param {DomainDeprecationDetails} details + */ + isDeprecationResolvable: (details: DomainDeprecationDetails) => boolean; + /** + * Calls the correctiveActions.api to automatically resolve the depprecation. + * + * @param {DomainDeprecationDetails} details + */ + resolveDeprecation: (details: DomainDeprecationDetails) => Promise; +} + +export class DeprecationsService implements CoreService { + public setup(): void {} + + public start({ http }: { http: HttpStart }): DeprecationsServiceStart { + const deprecationsClient = new DeprecationsClient({ http }); + + return { + getAllDeprecations: deprecationsClient.getAllDeprecations, + getDeprecations: deprecationsClient.getDeprecations, + isDeprecationResolvable: deprecationsClient.isDeprecationResolvable, + resolveDeprecation: deprecationsClient.resolveDeprecation, + }; + } + + public stop(): void {} +} diff --git a/src/core/server/utils/from_root.ts b/src/core/public/deprecations/index.ts similarity index 63% rename from src/core/server/utils/from_root.ts rename to src/core/public/deprecations/index.ts index 377f4d0e29ca5..092cbed613ac2 100644 --- a/src/core/server/utils/from_root.ts +++ b/src/core/public/deprecations/index.ts @@ -6,9 +6,6 @@ * Side Public License, v 1. */ -import { resolve } from 'path'; -import { pkg } from './package_json'; - -export function fromRoot(...args: string[]) { - return resolve(pkg.__dirname, ...args); -} +export { DeprecationsService } from './deprecations_service'; +export type { DeprecationsServiceStart } from './deprecations_service'; +export type { ResolveDeprecationResponse } from './deprecations_client'; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index ef3172b620b23..b179c998f1126 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -216,6 +216,7 @@ export class DocLinksService { }, maps: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, + importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, @@ -271,8 +272,10 @@ export class DocLinksService { painlessExecute: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, painlessExecuteAPIContexts: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html#_contexts`, putComponentTemplateMetadata: `${ELASTICSEARCH_DOCS}indices-component-template.html#component-templates-metadata`, + putEnrichPolicy: `${ELASTICSEARCH_DOCS}put-enrich-policy-api.html`, putSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, - putWatch: `${ELASTICSEARCH_DOCS}/watcher-api-put-watch.html`, + putWatch: `${ELASTICSEARCH_DOCS}watcher-api-put-watch.html`, + simulatePipeline: `${ELASTICSEARCH_DOCS}simulate-pipeline-api.html`, updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`, }, plugins: { @@ -293,9 +296,47 @@ export class DocLinksService { restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`, }, ingest: { + append: `${ELASTICSEARCH_DOCS}append-processor.html`, + bytes: `${ELASTICSEARCH_DOCS}bytes-processor.html`, + circle: `${ELASTICSEARCH_DOCS}ingest-circle-processor.html`, + convert: `${ELASTICSEARCH_DOCS}convert-processor.html`, + csv: `${ELASTICSEARCH_DOCS}csv-processor.html`, + date: `${ELASTICSEARCH_DOCS}date-processor.html`, + dateIndexName: `${ELASTICSEARCH_DOCS}date-index-name-processor.html`, + dissect: `${ELASTICSEARCH_DOCS}dissect-processor.html`, + dissectKeyModifiers: `${ELASTICSEARCH_DOCS}dissect-processor.html#dissect-key-modifiers`, + dotExpander: `${ELASTICSEARCH_DOCS}dot-expand-processor.html`, + drop: `${ELASTICSEARCH_DOCS}drop-processor.html`, + enrich: `${ELASTICSEARCH_DOCS}ingest-enriching-data.html`, + fail: `${ELASTICSEARCH_DOCS}fail-processor.html`, + foreach: `${ELASTICSEARCH_DOCS}foreach-processor.html`, + geoIp: `${ELASTICSEARCH_DOCS}geoip-processor.html`, + grok: `${ELASTICSEARCH_DOCS}grok-processor.html`, + gsub: `${ELASTICSEARCH_DOCS}gsub-processor.html`, + htmlString: `${ELASTICSEARCH_DOCS}htmlstrip-processor.html`, + inference: `${ELASTICSEARCH_DOCS}inference-processor.html`, + inferenceClassification: `${ELASTICSEARCH_DOCS}inference-processor.html#inference-processor-classification-opt`, + inferenceRegression: `${ELASTICSEARCH_DOCS}inference-processor.html#inference-processor-regression-opt`, + join: `${ELASTICSEARCH_DOCS}join-processor.html`, + json: `${ELASTICSEARCH_DOCS}json-processor.html`, + kv: `${ELASTICSEARCH_DOCS}kv-processor.html`, + lowercase: `${ELASTICSEARCH_DOCS}lowercase-processor.html`, + pipeline: `${ELASTICSEARCH_DOCS}pipeline-processor.html`, pipelines: `${ELASTICSEARCH_DOCS}ingest.html`, pipelineFailure: `${ELASTICSEARCH_DOCS}ingest.html#handling-pipeline-failures`, processors: `${ELASTICSEARCH_DOCS}processors.html`, + remove: `${ELASTICSEARCH_DOCS}remove-processor.html`, + rename: `${ELASTICSEARCH_DOCS}rename-processor.html`, + script: `${ELASTICSEARCH_DOCS}script-processor.html`, + set: `${ELASTICSEARCH_DOCS}set-processor.html`, + setSecurityUser: `${ELASTICSEARCH_DOCS}ingest-node-set-security-user-processor.html`, + sort: `${ELASTICSEARCH_DOCS}sort-processor.html`, + split: `${ELASTICSEARCH_DOCS}split-processor.html`, + trim: `${ELASTICSEARCH_DOCS}trim-processor.html`, + uppercase: `${ELASTICSEARCH_DOCS}uppercase-processor.html`, + uriParts: `${ELASTICSEARCH_DOCS}uri-parts-processor.html`, + urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`, + userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`, }, }, }); @@ -443,6 +484,7 @@ export interface DocLinksStart { putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; + simulatePipeline: string; updateTransform: string; }>; readonly observability: Record; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index c7b4c370eb6d7..750f2e27dc950 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -65,6 +65,7 @@ import { UiSettingsState, IUiSettingsClient } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; +import { DeprecationsServiceStart } from './deprecations'; export type { PackageInfo, EnvironmentMode, IExternalUrlPolicy } from '../server/types'; export type { CoreContext, CoreSystem } from './core_system'; @@ -184,6 +185,8 @@ export type { ErrorToastOptions, } from './notifications'; +export type { DeprecationsServiceStart, ResolveDeprecationResponse } from './deprecations'; + export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; export { URL_MAX_LENGTH } from './core_app'; @@ -268,6 +271,8 @@ export interface CoreStart { uiSettings: IUiSettingsClient; /** {@link FatalErrorsStart} */ fatalErrors: FatalErrorsStart; + /** {@link DeprecationsServiceStart} */ + deprecations: DeprecationsServiceStart; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index e47de84ea12b2..bd7623beba651 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -24,6 +24,7 @@ import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; +import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -37,6 +38,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; +export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; function createCoreSetupMock({ basePath = '', @@ -57,6 +59,7 @@ function createCoreSetupMock({ http: httpServiceMock.createSetupContract({ basePath }), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), + deprecations: deprecationsServiceMock.createSetupContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createSetupContract().getInjectedVar, }, @@ -76,6 +79,7 @@ function createCoreStartMock({ basePath = '' } = {}) { overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), + deprecations: deprecationsServiceMock.createStartContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index b59516fa121fb..49c895aa80fc4 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -139,5 +139,6 @@ export function createPluginStartContext< getInjectedVar: deps.injectedMetadata.getInjectedVar, }, fatalErrors: deps.fatalErrors, + deprecations: deps.deprecations, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index e70b78f237d75..d7114f14e2f00 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -34,6 +34,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; +import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; export let mockPluginInitializers: Map; @@ -101,6 +102,7 @@ describe('PluginsService', () => { uiSettings: uiSettingsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), + deprecations: deprecationsServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 5a5ae253bac7f..8327428991e13 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -432,6 +432,8 @@ export interface CoreStart { // (undocumented) chrome: ChromeStart; // (undocumented) + deprecations: DeprecationsServiceStart; + // (undocumented) docLinks: DocLinksStart; // (undocumented) fatalErrors: FatalErrorsStart; @@ -472,6 +474,15 @@ export class CoreSystem { // @internal (undocumented) export const DEFAULT_APP_CATEGORIES: Record; +// @public +export interface DeprecationsServiceStart { + // Warning: (ae-forgotten-export) The symbol "DomainDeprecationDetails" needs to be exported by the entry point index.d.ts + getAllDeprecations: () => Promise; + getDeprecations: (domainId: string) => Promise; + isDeprecationResolvable: (details: DomainDeprecationDetails) => boolean; + resolveDeprecation: (details: DomainDeprecationDetails) => Promise; +} + // @public (undocumented) export interface DocLinksStart { // (undocumented) @@ -616,6 +627,7 @@ export interface DocLinksStart { putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putWatch: string; + simulatePipeline: string; updateTransform: string; }>; readonly observability: Record; @@ -1075,6 +1087,16 @@ export type PublicAppSearchDeepLinkInfo = Omit; +// Warning: (ae-missing-release-tag) "ResolveDeprecationResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ResolveDeprecationResponse = { + status: 'ok'; +} | { + status: 'fail'; + reason: string; +}; + // Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1586,6 +1608,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:164:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index de13785a17f5b..ed2d9bc0b3917 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -11,6 +11,16 @@ min-height: 100%; } +#app-fixed-viewport { + pointer-events: none; + visibility: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + .app-wrapper { display: flex; flex-flow: column nowrap; @@ -35,6 +45,10 @@ @mixin kbnAffordForHeader($headerHeight) { padding-top: $headerHeight; + #app-fixed-viewport { + top: $headerHeight; + } + .euiFlyout, .euiCollapsibleNav { top: $headerHeight; diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 843f2a253f33e..787fa475c7d5f 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -52,6 +52,7 @@ export class RenderingService { {chromeHeader}
+
{bannerComponent}
{appComponent}
diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index b6b3ab5b8face..e3c236405a596 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -20,7 +20,7 @@ const applyCoreDeprecations = (settings: Record = {}) => { deprecation, path: '', })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, @@ -305,7 +305,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -315,7 +315,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + "\\"logging.dest\\" has been deprecated and will be removed in 8.0. To set the destination moving forward, you can use the \\"console\\" appender in your logging configuration or define a custom one. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -361,7 +361,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.", + "\\"logging.json\\" has been deprecated and will be removed in 8.0. To specify log message format moving forward, you can configure the \\"appender.layout\\" property for every custom appender in your logging configuration. There is currently no default layout for custom appenders and each one must be declared explicitly. For more details, see https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx", ] `); }); @@ -446,7 +446,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.filter\\" has been deprecated and will be removed in 8.0. ", + "\\"logging.filter\\" has been deprecated and will be removed in 8.0.", ] `); }); @@ -457,7 +457,7 @@ describe('core deprecations', () => { }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"logging.filter\\" has been deprecated and will be removed in 8.0. ", + "\\"logging.filter\\" has been deprecated and will be removed in 8.0.", ] `); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 565b957b2a8e1..2e77374e3068a 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -9,40 +9,43 @@ import { has, get } from 'lodash'; import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config'; -const configPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const configPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(process.env, 'CONFIG_PATH')) { - log( - `Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder` - ); + addDeprecation({ + message: `Environment variable CONFIG_PATH is deprecated. It has been replaced with KBN_PATH_CONF pointing to a config folder`, + }); } return settings; }; -const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(process.env, 'DATA_PATH')) { - log( - `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"` - ); + addDeprecation({ + message: `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`, + }); } return settings; }; -const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { - log( - 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + + addDeprecation({ + message: + 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + - 'current behavior and silence this warning.' - ); + 'current behavior and silence this warning.', + }); } return settings; }; -const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, log) => { +const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecation) => { const corsSettings = get(settings, 'server.cors'); if (typeof get(settings, 'server.cors') === 'boolean') { - log('"server.cors" is deprecated and has been replaced by "server.cors.enabled"'); + addDeprecation({ + message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"', + }); settings.server.cors = { enabled: corsSettings, }; @@ -50,7 +53,7 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; -const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { const NONCE_STRING = `{nonce}`; // Policies that should include the 'self' source const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); @@ -67,7 +70,9 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { settings.csp.rules = [...parsed].map(([policy, sourceList]) => { if (sourceList.find((source) => source.includes(NONCE_STRING))) { - log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); + addDeprecation({ + message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, + }); sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING)); // Add 'self' if not present @@ -80,7 +85,9 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { SELF_POLICIES.includes(policy) && !sourceList.find((source) => source.includes(SELF_STRING)) ) { - log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); + addDeprecation({ + message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, + }); sourceList.push(SELF_STRING); } @@ -91,149 +98,191 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; -const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( + settings, + fromPath, + addDeprecation +) => { if (has(settings, 'map.manifestServiceUrl')) { - log( - 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + + addDeprecation({ + message: + 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + 'of the Elastic Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' + '"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' + - 'modified for use in production environments.' - ); + 'modified for use in production environments.', + }); } return settings; }; -const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const opsLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.ops')) { - log( - '"logging.events.ops" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.ops" has been deprecated and will be removed ' + 'in 8.0. To access ops data moving forward, please enable debug logs for the ' + '"metrics.ops" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const requestLoggingEventDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.request') || has(settings, 'logging.events.response')) { - log( - '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.request" and "logging.events.response" have been deprecated and will be removed ' + 'in 8.0. To access request and/or response data moving forward, please enable debug logs for the ' + '"http.server.response" context in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const timezoneLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.timezone')) { - log( - '"logging.timezone" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingtimezone', + message: + '"logging.timezone" has been deprecated and will be removed ' + 'in 8.0. To set the timezone moving forward, please add a timezone date modifier to the log pattern ' + 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const destLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.dest')) { - log( - '"logging.dest" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingdest', + message: + '"logging.dest" has been deprecated and will be removed ' + 'in 8.0. To set the destination moving forward, you can use the "console" appender ' + 'in your logging configuration or define a custom one. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const quietLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.quiet')) { - log( - '"logging.quiet" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingquiet', + message: + '"logging.quiet" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:error" in your logging configuration. ', + }); } return settings; }; -const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const silentLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.silent')) { - log( - '"logging.silent" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingsilent', + message: + '"logging.silent" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:off" in your logging configuration. ', + }); } return settings; }; -const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const verboseLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.verbose')) { - log( - '"logging.verbose" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingverbose', + message: + '"logging.verbose" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level:all" in your logging configuration. ', + }); } return settings; }; -const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const jsonLoggingDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { // We silence the deprecation warning when running in development mode because // the dev CLI code in src/dev/cli_dev_mode/using_server_process.ts manually // specifies `--logging.json=false`. Since it's executed in a child process, the // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY` if (has(settings, 'logging.json') && settings.env !== 'development') { - log( - '"logging.json" has been deprecated and will be removed ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + message: + '"logging.json" has been deprecated and will be removed ' + 'in 8.0. To specify log message format moving forward, ' + 'you can configure the "appender.layout" property for every custom appender in your logging configuration. ' + 'There is currently no default layout for custom appenders and each one must be declared explicitly. ' + 'For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx.' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx', + }); } return settings; }; -const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logRotateDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.rotate')) { - log( - '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' + + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', + message: + '"logging.rotate" and sub-options have been deprecated and will be removed in 8.0. ' + 'Moving forward, you can enable log rotation using the "rolling-file" appender for a logger ' + 'in your logging configuration. For more details, see ' + - 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender' - ); + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#rolling-file-appender', + }); } return settings; }; -const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logEventsLogDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.log')) { - log( - '"logging.events.log" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.log" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, log levels can be customized on a per-logger basis using the new logging configuration. ', + }); } return settings; }; -const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logEventsErrorDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.events.error')) { - log( - '"logging.events.error" has been deprecated and will be removed ' + - 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ' - ); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingevents', + message: + '"logging.events.error" has been deprecated and will be removed ' + + 'in 8.0. Moving forward, you can use "logging.root.level: error" in your logging configuration. ', + }); } return settings; }; -const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, log) => { +const logFilterDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (has(settings, 'logging.filter')) { - log('"logging.filter" has been deprecated and will be removed ' + 'in 8.0. '); + addDeprecation({ + documentationUrl: + 'https://github.com/elastic/kibana/blob/master/src/core/server/logging/README.mdx#loggingfilter', + message: '"logging.filter" has been deprecated and will be removed in 8.0.', + }); } return settings; }; diff --git a/src/core/server/config/ensure_valid_configuration.test.ts b/src/core/server/config/ensure_valid_configuration.test.ts new file mode 100644 index 0000000000000..474e8dd59b4c4 --- /dev/null +++ b/src/core/server/config/ensure_valid_configuration.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { configServiceMock } from './mocks'; +import { ensureValidConfiguration } from './ensure_valid_configuration'; +import { CriticalError } from '../errors'; + +describe('ensureValidConfiguration', () => { + let configService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + configService = configServiceMock.create(); + configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic'])); + }); + + it('returns normally when there is no unused keys', async () => { + configService.getUnusedPaths.mockResolvedValue([]); + await expect(ensureValidConfiguration(configService as any)).resolves.toBeUndefined(); + }); + + it('throws when there are some unused keys', async () => { + configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']); + + await expect(ensureValidConfiguration(configService as any)).rejects.toMatchInlineSnapshot( + `[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]` + ); + }); + + it('throws a `CriticalError` with the correct processExitCode value', async () => { + expect.assertions(2); + + configService.getUnusedPaths.mockResolvedValue(['some.key', 'some.other.key']); + + try { + await ensureValidConfiguration(configService as any); + } catch (e) { + expect(e).toBeInstanceOf(CriticalError); + expect(e.processExitCode).toEqual(64); + } + }); +}); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/config/ensure_valid_configuration.ts similarity index 62% rename from src/core/server/legacy/config/ensure_valid_configuration.ts rename to src/core/server/config/ensure_valid_configuration.ts index fd3dd29e3d354..a33625cc0841d 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/config/ensure_valid_configuration.ts @@ -6,20 +6,13 @@ * Side Public License, v 1. */ -import { getUnusedConfigKeys } from './get_unused_config_keys'; -import { ConfigService } from '../../config'; -import { CriticalError } from '../../errors'; -import { LegacyServiceSetupConfig } from '../types'; +import { ConfigService } from '@kbn/config'; +import { CriticalError } from '../errors'; -export async function ensureValidConfiguration( - configService: ConfigService, - { legacyConfig, settings }: LegacyServiceSetupConfig -) { - const unusedConfigKeys = await getUnusedConfigKeys({ - coreHandledConfigPaths: await configService.getUsedPaths(), - settings, - legacyConfig, - }); +export async function ensureValidConfiguration(configService: ConfigService) { + await configService.validate(); + + const unusedConfigKeys = await configService.getUnusedPaths(); if (unusedConfigKeys.length > 0) { const message = `Unknown configuration key(s): ${unusedConfigKeys diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index bf2ce16e869b7..686564c6d678a 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -7,6 +7,7 @@ */ export { coreDeprecationProvider } from './deprecation'; +export { ensureValidConfiguration } from './ensure_valid_configuration'; export { ConfigService, @@ -24,7 +25,7 @@ export type { ConfigPath, CliArgs, ConfigDeprecation, - ConfigDeprecationLogger, + AddConfigDeprecation, ConfigDeprecationProvider, ConfigDeprecationFactory, EnvironmentMode, diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts index d51c369146957..830f4a9a94364 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts @@ -10,7 +10,7 @@ import { registerRouteForBundleMock } from './register_bundle_routes.test.mocks' import { PackageInfo } from '@kbn/config'; import { httpServiceMock } from '../../http/http_service.mock'; -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { registerBundleRoutes } from './register_bundle_routes'; import { FileHashCache } from './file_hash_cache'; @@ -29,9 +29,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({ internal: ids.reduce((map, id) => { map.set(id, { publicTargetDir: `/plugins/${id}/public-target-dir`, + publicAssetsDir: `/plugins/${id}/public-assets-dir`, + version: '8.0.0', + requiredBundles: [], }); return map; - }, new Map()), + }, new Map()), }); describe('registerBundleRoutes', () => { @@ -86,16 +89,16 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-a/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-a/', - routePath: '/42/bundles/plugin/plugin-a/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/', + routePath: '/42/bundles/plugin/plugin-a/8.0.0/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-b/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-b/', - routePath: '/42/bundles/plugin/plugin-b/', + publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/', + routePath: '/42/bundles/plugin/plugin-b/8.0.0/', }); }); }); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts index ee54f8ef34622..f313f10003631 100644 --- a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -8,10 +8,10 @@ import { join } from 'path'; import { PackageInfo } from '@kbn/config'; +import { fromRoot } from '@kbn/utils'; import { distDir as uiSharedDepsDistDir } from '@kbn/ui-shared-deps'; import { IRouter } from '../../http'; import { UiPlugins } from '../../plugins'; -import { fromRoot } from '../../utils'; import { FileHashCache } from './file_hash_cache'; import { registerRouteForBundle } from './bundles_route'; @@ -27,7 +27,7 @@ import { registerRouteForBundle } from './bundles_route'; */ export function registerBundleRoutes({ router, - serverBasePath, // serverBasePath + serverBasePath, uiPlugins, packageInfo, }: { @@ -57,10 +57,10 @@ export function registerBundleRoutes({ isDist, }); - [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir }]) => { + [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => { registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/`, - routePath: `/${buildNum}/bundles/plugin/${id}/`, + publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`, + routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`, bundlesPath: publicTargetDir, fileHashCache, isDist, diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index dac941767ebb5..bc1098832bac5 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -7,9 +7,11 @@ */ import Path from 'path'; +import { stringify } from 'querystring'; import { Env } from '@kbn/config'; +import { schema } from '@kbn/config-schema'; +import { fromRoot } from '@kbn/utils'; -import { fromRoot } from '../utils'; import { InternalCoreSetup } from '../internal_types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; @@ -49,6 +51,41 @@ export class CoreApp { }); }); + // remove trailing slash catch-all + router.get( + { + path: '/{path*}', + validate: { + params: schema.object({ + path: schema.maybe(schema.string()), + }), + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + }, + }, + async (context, req, res) => { + const { query, params } = req; + const { path } = params; + if (!path || !path.endsWith('/')) { + return res.notFound(); + } + + const basePath = httpSetup.basePath.get(req); + let rewrittenPath = path.slice(0, -1); + if (`/${path}`.startsWith(basePath)) { + rewrittenPath = rewrittenPath.substring(basePath.length); + } + + const querystring = query ? stringify(query) : undefined; + const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`; + + return res.redirected({ + headers: { + location: url, + }, + }); + } + ); + router.get({ path: '/core', validate: false }, async (context, req, res) => res.ok({ body: { version: '0.0.1' } }) ); diff --git a/src/core/server/core_app/integration_tests/core_app_routes.test.ts b/src/core/server/core_app/integration_tests/core_app_routes.test.ts new file mode 100644 index 0000000000000..6b0643f7d1bc7 --- /dev/null +++ b/src/core/server/core_app/integration_tests/core_app_routes.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 * as kbnTestServer from '../../../test_helpers/kbn_server'; +import { Root } from '../../root'; + +describe('Core app routes', () => { + let root: Root; + + beforeAll(async function () { + root = kbnTestServer.createRoot({ + plugins: { initialize: false }, + server: { + basePath: '/base-path', + }, + }); + + await root.setup(); + await root.start(); + }); + + afterAll(async function () { + await root.shutdown(); + }); + + describe('`/{path*}` route', () => { + it('redirects requests to include the basePath', async () => { + const response = await kbnTestServer.request.get(root, '/some-path/').expect(302); + expect(response.get('location')).toEqual('/base-path/some-path'); + }); + + it('includes the query in the redirect', async () => { + const response = await kbnTestServer.request.get(root, '/some-path/?foo=bar').expect(302); + expect(response.get('location')).toEqual('/base-path/some-path?foo=bar'); + }); + + it('does not redirect if the path does not end with `/`', async () => { + await kbnTestServer.request.get(root, '/some-path').expect(404); + }); + + it('does not add the basePath if the path already contains it', async () => { + const response = await kbnTestServer.request.get(root, '/base-path/foo/').expect(302); + expect(response.get('location')).toEqual('/base-path/foo'); + }); + }); + + describe('`/` route', () => { + it('prevails on the `/{path*}` route', async () => { + const response = await kbnTestServer.request.get(root, '/').expect(302); + expect(response.get('location')).toEqual('/base-path/app/home'); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_factory.test.ts b/src/core/server/deprecations/deprecations_factory.test.ts new file mode 100644 index 0000000000000..469451b0020c0 --- /dev/null +++ b/src/core/server/deprecations/deprecations_factory.test.ts @@ -0,0 +1,248 @@ +/* + * Copyright 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 { GetDeprecationsContext } from './types'; +import { DeprecationsFactory } from './deprecations_factory'; +import { loggerMock } from '../logging/logger.mock'; + +describe('DeprecationsFactory', () => { + const logger = loggerMock.create(); + beforeEach(() => { + loggerMock.clear(logger); + }); + + describe('getRegistry', () => { + const domainId = 'test-plugin'; + + it('creates a registry for a domainId', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const registry = deprecationsFactory.getRegistry(domainId); + + expect(registry).toHaveProperty('registerDeprecations'); + expect(registry).toHaveProperty('getDeprecations'); + }); + + it('creates one registry for a domainId', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const registry = deprecationsFactory.getRegistry(domainId); + const sameRegistry = deprecationsFactory.getRegistry(domainId); + + expect(registry).toStrictEqual(sameRegistry); + }); + + it('returns a registered registry', () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const mockRegistry = 'mock-reg'; + const mockRegistries = { + set: jest.fn(), + get: jest.fn().mockReturnValue(mockRegistry), + }; + + // @ts-expect-error + deprecationsFactory.registries = mockRegistries; + const result = deprecationsFactory.getRegistry(domainId); + + expect(mockRegistries.get).toBeCalledTimes(1); + expect(mockRegistries.get).toBeCalledWith(domainId); + expect(mockRegistries.set).toBeCalledTimes(0); + expect(result).toStrictEqual(mockRegistry); + }); + }); + + describe('getAllDeprecations', () => { + const mockDependencies = ({ + esClient: jest.fn(), + savedObjectsClient: jest.fn(), + } as unknown) as GetDeprecationsContext; + + it('returns a flattened array of deprecations', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const mockPluginDeprecationsInfo = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + { + message: 'hello there!', + level: 'warning', + correctiveActions: { + manualSteps: ['mockPlugin step a', 'mockPlugin step b'], + }, + }, + ]; + const anotherMockPluginDeprecationsInfo = [ + { + message: 'anotherMockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['anotherMockPlugin step 1', 'anotherMockPlugin step 2'], + }, + }, + ]; + + const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const anotherMockPluginRegistry = deprecationsFactory.getRegistry('anotherMockPlugin'); + mockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(mockPluginDeprecationsInfo), + }); + anotherMockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(anotherMockPluginDeprecationsInfo), + }); + + const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + expect(derpecations).toStrictEqual( + [ + mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), + anotherMockPluginDeprecationsInfo.map((info) => ({ + ...info, + domainId: 'anotherMockPlugin', + })), + ].flat() + ); + }); + + it(`returns a failure message for failed getDeprecations functions`, async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const domainId = 'mockPlugin'; + const mockError = new Error(); + + const deprecationsRegistry = deprecationsFactory.getRegistry(domainId); + deprecationsRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockRejectedValue(mockError), + }); + const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + expect(logger.warn).toBeCalledTimes(1); + expect(logger.warn).toBeCalledWith( + `Failed to get deprecations info for plugin "${domainId}".`, + mockError + ); + expect(derpecations).toStrictEqual([ + { + domainId, + message: `Failed to get deprecations info for plugin "${domainId}".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]); + }); + + it(`returns successful results even when some getDeprecations fail`, async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const anotherMockPluginRegistry = deprecationsFactory.getRegistry('anotherMockPlugin'); + const mockError = new Error(); + const mockPluginDeprecationsInfo = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + ]; + mockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(mockPluginDeprecationsInfo), + }); + anotherMockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockRejectedValue(mockError), + }); + const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + + expect(logger.warn).toBeCalledTimes(1); + expect(logger.warn).toBeCalledWith( + `Failed to get deprecations info for plugin "anotherMockPlugin".`, + mockError + ); + expect(derpecations).toStrictEqual([ + ...mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), + { + domainId: 'anotherMockPlugin', + message: `Failed to get deprecations info for plugin "anotherMockPlugin".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]); + }); + }); + + describe('getDeprecations', () => { + const mockDependencies = ({ + esClient: jest.fn(), + savedObjectsClient: jest.fn(), + } as unknown) as GetDeprecationsContext; + + it('returns a flattened array of DeprecationInfo', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const deprecationsBody = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + [ + { + message: 'hello there!', + level: 'warning', + correctiveActions: { + manualSteps: ['mockPlugin step a', 'mockPlugin step b'], + }, + }, + ], + ]; + + deprecationsRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(deprecationsBody), + }); + + const derpecations = await deprecationsFactory.getDeprecations( + 'mockPlugin', + mockDependencies + ); + expect(derpecations).toStrictEqual( + deprecationsBody.flat().map((body) => ({ ...body, domainId: 'mockPlugin' })) + ); + }); + + it('removes empty entries from the returned array', async () => { + const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsRegistry = deprecationsFactory.getRegistry('mockPlugin'); + const deprecationsBody = [ + { + message: 'mockPlugin message', + level: 'critical', + correctiveActions: { + manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + }, + }, + [undefined], + undefined, + ]; + + deprecationsRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(deprecationsBody), + }); + + const derpecations = await deprecationsFactory.getDeprecations( + 'mockPlugin', + mockDependencies + ); + expect(derpecations).toHaveLength(1); + expect(derpecations).toStrictEqual([{ ...deprecationsBody[0], domainId: 'mockPlugin' }]); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_factory.ts b/src/core/server/deprecations/deprecations_factory.ts new file mode 100644 index 0000000000000..3699c088e20f1 --- /dev/null +++ b/src/core/server/deprecations/deprecations_factory.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 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 { DeprecationsRegistry } from './deprecations_registry'; +import type { Logger } from '../logging'; +import type { + DomainDeprecationDetails, + DeprecationsDetails, + GetDeprecationsContext, +} from './types'; + +export interface DeprecationsFactoryDeps { + logger: Logger; +} + +export class DeprecationsFactory { + private readonly registries: Map = new Map(); + private readonly logger: Logger; + constructor({ logger }: DeprecationsFactoryDeps) { + this.logger = logger; + } + + public getRegistry = (domainId: string): DeprecationsRegistry => { + const existing = this.registries.get(domainId); + if (existing) { + return existing; + } + const registry = new DeprecationsRegistry(); + this.registries.set(domainId, registry); + return registry; + }; + + public getDeprecations = async ( + domainId: string, + dependencies: GetDeprecationsContext + ): Promise => { + const infoBody = await this.getDeprecationsBody(domainId, dependencies); + return this.createDeprecationInfo(domainId, infoBody).flat(); + }; + + public getAllDeprecations = async ( + dependencies: GetDeprecationsContext + ): Promise => { + const domainIds = [...this.registries.keys()]; + + const deprecationsInfo = await Promise.all( + domainIds.map(async (domainId) => { + const infoBody = await this.getDeprecationsBody(domainId, dependencies); + return this.createDeprecationInfo(domainId, infoBody); + }) + ); + + return deprecationsInfo.flat(); + }; + + private createDeprecationInfo = ( + domainId: string, + deprecationInfoBody: DeprecationsDetails[] + ): DomainDeprecationDetails[] => { + return deprecationInfoBody + .flat() + .filter(Boolean) + .map((pluginDeprecation) => ({ + ...pluginDeprecation, + domainId, + })); + }; + + private getDeprecationsBody = async ( + domainId: string, + dependencies: GetDeprecationsContext + ): Promise => { + const deprecationsRegistry = this.registries.get(domainId); + if (!deprecationsRegistry) { + return []; + } + try { + const settledResults = await deprecationsRegistry.getDeprecations(dependencies); + return settledResults.flatMap((settledResult) => { + if (settledResult.status === 'rejected') { + this.logger.warn( + `Failed to get deprecations info for plugin "${domainId}".`, + settledResult.reason + ); + return [ + { + message: `Failed to get deprecations info for plugin "${domainId}".`, + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]; + } + + return settledResult.value; + }); + } catch (err) { + this.logger.warn(`Failed to get deprecations info for plugin "${domainId}".`, err); + return []; + } + }; +} diff --git a/src/core/server/deprecations/deprecations_registry.test.ts b/src/core/server/deprecations/deprecations_registry.test.ts new file mode 100644 index 0000000000000..507677a531861 --- /dev/null +++ b/src/core/server/deprecations/deprecations_registry.test.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 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. + */ + +/* eslint-disable dot-notation */ +import { RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; +import { DeprecationsRegistry } from './deprecations_registry'; + +describe('DeprecationsRegistry', () => { + describe('registerDeprecations', () => { + it('throws if getDeprecations is not a function', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const deprecationsConfig = ({ + getDeprecations: null, + } as unknown) as RegisterDeprecationsConfig; + expect(() => deprecationsRegistry.registerDeprecations(deprecationsConfig)).toThrowError( + /getDeprecations must be a function/ + ); + }); + + it('registers deprecation context', () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const getDeprecations = jest.fn(); + const deprecationsConfig = { getDeprecations }; + deprecationsRegistry.registerDeprecations(deprecationsConfig); + expect(deprecationsRegistry['deprecationContexts']).toStrictEqual([deprecationsConfig]); + }); + + it('allows registering multiple contexts', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const deprecationsConfigA = { getDeprecations: jest.fn() }; + const deprecationsConfigB = { getDeprecations: jest.fn() }; + deprecationsRegistry.registerDeprecations(deprecationsConfigA); + deprecationsRegistry.registerDeprecations(deprecationsConfigB); + expect(deprecationsRegistry['deprecationContexts']).toStrictEqual([ + deprecationsConfigA, + deprecationsConfigB, + ]); + }); + }); + + describe('getDeprecations', () => { + it('returns all settled deprecations', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const mockContext = ({} as unknown) as GetDeprecationsContext; + const mockError = new Error(); + const deprecationsConfigA = { getDeprecations: jest.fn().mockResolvedValue('hi') }; + const deprecationsConfigB = { getDeprecations: jest.fn().mockRejectedValue(mockError) }; + deprecationsRegistry.registerDeprecations(deprecationsConfigA); + deprecationsRegistry.registerDeprecations(deprecationsConfigB); + const deprecations = await deprecationsRegistry.getDeprecations(mockContext); + expect(deprecations).toStrictEqual([ + { + status: 'fulfilled', + value: 'hi', + }, + { + status: 'rejected', + reason: mockError, + }, + ]); + }); + + it('passes dependencies to registered getDeprecations function', async () => { + const deprecationsRegistry = new DeprecationsRegistry(); + const mockContext = ({} as unknown) as GetDeprecationsContext; + const deprecationsConfig = { getDeprecations: jest.fn().mockResolvedValue('hi') }; + deprecationsRegistry.registerDeprecations(deprecationsConfig); + const deprecations = await deprecationsRegistry.getDeprecations(mockContext); + expect(deprecations).toHaveLength(1); + expect(deprecationsConfig.getDeprecations).toBeCalledWith(mockContext); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_registry.ts b/src/core/server/deprecations/deprecations_registry.ts new file mode 100644 index 0000000000000..f92d807514b82 --- /dev/null +++ b/src/core/server/deprecations/deprecations_registry.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 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 { DeprecationsDetails, RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; + +export class DeprecationsRegistry { + private readonly deprecationContexts: RegisterDeprecationsConfig[] = []; + + public registerDeprecations = (deprecationContext: RegisterDeprecationsConfig) => { + if (typeof deprecationContext.getDeprecations !== 'function') { + throw new Error(`getDeprecations must be a function in registerDeprecations(context)`); + } + + this.deprecationContexts.push(deprecationContext); + }; + + public getDeprecations = async ( + dependencies: GetDeprecationsContext + ): Promise>> => { + return await Promise.allSettled( + this.deprecationContexts.map( + async (deprecationContext) => await deprecationContext.getDeprecations(dependencies) + ) + ); + }; +} diff --git a/src/core/server/deprecations/deprecations_service.mock.ts b/src/core/server/deprecations/deprecations_service.mock.ts new file mode 100644 index 0000000000000..c0febf90a489a --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.mock.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { + DeprecationsService, + InternalDeprecationsServiceSetup, + DeprecationsServiceSetup, +} from './deprecations_service'; +type DeprecationsServiceContract = PublicMethodsOf; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + registerDeprecations: jest.fn(), + }; + + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const internalSetupContract: jest.Mocked = { + getRegistry: jest.fn(), + }; + + internalSetupContract.getRegistry.mockReturnValue(createSetupContractMock()); + return internalSetupContract; +}; + +const createDeprecationsServiceMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockReturnValue(createInternalSetupContractMock()); + return mocked; +}; + +export const deprecationsServiceMock = { + create: createDeprecationsServiceMock, + createInternalSetupContract: createInternalSetupContractMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts new file mode 100644 index 0000000000000..8eca1ba5790c5 --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.ts @@ -0,0 +1,168 @@ +/* + * Copyright 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 { DeprecationsFactory } from './deprecations_factory'; +import { RegisterDeprecationsConfig } from './types'; +import { registerRoutes } from './routes'; + +import { CoreContext } from '../core_context'; +import { CoreUsageDataSetup } from '../core_usage_data'; +import { InternalElasticsearchServiceSetup } from '../elasticsearch'; +import { CoreService } from '../../types'; +import { InternalHttpServiceSetup } from '../http'; +import { Logger } from '../logging'; + +/** + * The deprecations service provides a way for the Kibana platform to communicate deprecated + * features and configs with its users. These deprecations are only communicated + * if the deployment is using these features. Allowing for a user tailored experience + * for upgrading the stack version. + * + * The Deprecation service is consumed by the upgrade assistant to assist with the upgrade + * experience. + * + * If a deprecated feature can be resolved without manual user intervention. + * Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the + * deprecation upon a user trigger. + * + * @example + * ```ts + * import { DeprecationsDetails, GetDeprecationsContext, CoreSetup } from 'src/core/server'; + * + * async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { + * const deprecations: DeprecationsDetails[] = []; + * const count = await getTimelionSheetsCount(savedObjectsClient); + * + * if (count > 0) { + * // Example of a manual correctiveAction + * deprecations.push({ + * message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + * documentationUrl: + * 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', + * level: 'warning', + * correctiveActions: { + * manualSteps: [ + * 'Navigate to the Kibana Dashboard and click "Create dashboard".', + * 'Select Timelion from the "New Visualization" window.', + * 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', + * 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', + * 'In the toolbar, click Save.', + * 'On the Save visualization window, enter the visualization Title, then click Save and return.', + * ], + * }, + * }); + * } + * + * // Example of an api correctiveAction + * deprecations.push({ + * "message": "User 'test_dashboard_user' is using a deprecated role: 'kibana_user'", + * "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", + * "level": "critical", + * "correctiveActions": { + * "api": { + * "path": "/internal/security/users/test_dashboard_user", + * "method": "POST", + * "body": { + * "username": "test_dashboard_user", + * "roles": [ + * "machine_learning_user", + * "enrich_user", + * "kibana_admin" + * ], + * "full_name": "Alison Goryachev", + * "email": "alisongoryachev@gmail.com", + * "metadata": {}, + * "enabled": true + * } + * }, + * "manualSteps": [ + * "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", + * "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role." + * ] + * }, + * }); + * + * return deprecations; + * } + * + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.deprecations.registerDeprecations({ getDeprecations }); + * } + * } + * ``` + * + * @public + */ +export interface DeprecationsServiceSetup { + registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; +} + +/** @internal */ +export interface InternalDeprecationsServiceSetup { + getRegistry: (domainId: string) => DeprecationsServiceSetup; +} + +/** @internal */ +export interface DeprecationsSetupDeps { + http: InternalHttpServiceSetup; + elasticsearch: InternalElasticsearchServiceSetup; + coreUsageData: CoreUsageDataSetup; +} + +/** @internal */ +export class DeprecationsService implements CoreService { + private readonly logger: Logger; + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('deprecations-service'); + } + + public setup({ http }: DeprecationsSetupDeps): InternalDeprecationsServiceSetup { + this.logger.debug('Setting up Deprecations service'); + const deprecationsFactory = new DeprecationsFactory({ + logger: this.logger, + }); + + registerRoutes({ http, deprecationsFactory }); + this.registerConfigDeprecationsInfo(deprecationsFactory); + + return { + getRegistry: (domainId: string): DeprecationsServiceSetup => { + const registry = deprecationsFactory.getRegistry(domainId); + return { + registerDeprecations: registry.registerDeprecations, + }; + }, + }; + } + + public start() {} + public stop() {} + + private registerConfigDeprecationsInfo(deprecationsFactory: DeprecationsFactory) { + const handledDeprecatedConfigs = this.coreContext.configService.getHandledDeprecatedConfigs(); + + for (const [domainId, deprecationsContexts] of handledDeprecatedConfigs) { + const deprecationsRegistry = deprecationsFactory.getRegistry(domainId); + deprecationsRegistry.registerDeprecations({ + getDeprecations: () => { + return deprecationsContexts.map(({ message, correctiveActions, documentationUrl }) => { + return { + level: 'critical', + message, + correctiveActions: correctiveActions ?? {}, + documentationUrl, + }; + }); + }, + }); + } + } +} diff --git a/src/core/server/utils/package_json.ts b/src/core/server/deprecations/index.ts similarity index 53% rename from src/core/server/utils/package_json.ts rename to src/core/server/deprecations/index.ts index 57ca781d7d78e..c7d1a13800694 100644 --- a/src/core/server/utils/package_json.ts +++ b/src/core/server/deprecations/index.ts @@ -6,10 +6,16 @@ * Side Public License, v 1. */ -import { dirname } from 'path'; +export type { + DeprecationsDetails, + GetDeprecationsContext, + RegisterDeprecationsConfig, + DeprecationsGetResponse, +} from './types'; -export const pkg = { - __filename: require.resolve('../../../../package.json'), - __dirname: dirname(require.resolve('../../../../package.json')), - ...require('../../../../package.json'), -}; +export type { + DeprecationsServiceSetup, + InternalDeprecationsServiceSetup, +} from './deprecations_service'; + +export { DeprecationsService } from './deprecations_service'; diff --git a/src/core/server/deprecations/routes/get.ts b/src/core/server/deprecations/routes/get.ts new file mode 100644 index 0000000000000..fed3fcfbd1809 --- /dev/null +++ b/src/core/server/deprecations/routes/get.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { IRouter } from '../../http'; +import { GetDeprecationsContext, DeprecationsGetResponse } from '../types'; +import { DeprecationsFactory } from '../deprecations_factory'; + +interface RouteDependencies { + deprecationsFactory: DeprecationsFactory; +} + +export const registerGetRoute = (router: IRouter, { deprecationsFactory }: RouteDependencies) => { + router.get( + { + path: '/', + validate: false, + }, + async (context, req, res) => { + const dependencies: GetDeprecationsContext = { + esClient: context.core.elasticsearch.client, + savedObjectsClient: context.core.savedObjects.client, + }; + + const body: DeprecationsGetResponse = { + deprecations: await deprecationsFactory.getAllDeprecations(dependencies), + }; + + return res.ok({ body }); + } + ); +}; diff --git a/src/core/server/deprecations/routes/index.ts b/src/core/server/deprecations/routes/index.ts new file mode 100644 index 0000000000000..db58bec29f7b8 --- /dev/null +++ b/src/core/server/deprecations/routes/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { InternalHttpServiceSetup } from '../../http'; +import { registerGetRoute } from './get'; +import { DeprecationsFactory } from '../deprecations_factory'; + +export function registerRoutes({ + http, + deprecationsFactory, +}: { + http: InternalHttpServiceSetup; + deprecationsFactory: DeprecationsFactory; +}) { + const router = http.createRouter('/api/deprecations'); + registerGetRoute(router, { deprecationsFactory }); +} diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts new file mode 100644 index 0000000000000..31734b51b46bd --- /dev/null +++ b/src/core/server/deprecations/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsClientContract } from '../saved_objects/types'; +import type { IScopedClusterClient } from '../elasticsearch'; + +type MaybePromise = T | Promise; + +export interface DomainDeprecationDetails extends DeprecationsDetails { + domainId: string; +} + +export interface DeprecationsDetails { + /* The message to be displayed for the deprecation. */ + message: string; + /** + * levels: + * - warning: will not break deployment upon upgrade + * - critical: needs to be addressed before upgrade. + * - fetch_error: Deprecations service failed to grab the deprecation details for the domain. + */ + level: 'warning' | 'critical' | 'fetch_error'; + /* (optional) link to the documentation for more details on the deprecation. */ + documentationUrl?: string; + /* corrective action needed to fix this deprecation. */ + correctiveActions: { + /** + * (optional) The api to be called to automatically fix the deprecation + * Each domain should implement a POST/PUT route for their plugin to + * handle their deprecations. + */ + api?: { + /* Kibana route path. Passing a query string is allowed */ + path: string; + /* Kibana route method: 'POST' or 'PUT'. */ + method: 'POST' | 'PUT'; + /* Additional details to be passed to the route. */ + body?: { + [key: string]: any; + }; + }; + /** + * (optional) If this deprecation cannot be automtically fixed + * via an API corrective action. Specify a list of manual steps + * users need to follow to fix the deprecation before upgrade. + */ + manualSteps?: string[]; + }; +} + +export interface RegisterDeprecationsConfig { + getDeprecations: (context: GetDeprecationsContext) => MaybePromise; +} + +export interface GetDeprecationsContext { + esClient: IScopedClusterClient; + savedObjectsClient: SavedObjectsClientContract; +} + +export interface DeprecationsGetResponse { + deprecations: DomainDeprecationDetails[]; +} diff --git a/src/core/server/elasticsearch/elasticsearch_config.test.ts b/src/core/server/elasticsearch/elasticsearch_config.test.ts index 4b6cf220ccd52..23b804b535405 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.test.ts @@ -28,7 +28,7 @@ const applyElasticsearchDeprecations = (settings: Record = {}) => { deprecation, path: CONFIG_PATH, })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index d3432344f5a73..e731af4817955 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -144,32 +144,32 @@ export const configSchema = schema.object({ }); const deprecations: ConfigDeprecationProvider = () => [ - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const es = settings[fromPath]; if (!es) { return settings; } if (es.username === 'elastic') { - log( - `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.` - ); + addDeprecation({ + message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, + }); } else if (es.username === 'kibana') { - log( - `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.` - ); + addDeprecation({ + message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, + }); } if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { - log( - `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` - ); + addDeprecation({ + message: `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + }); } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { - log( - `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` - ); + addDeprecation({ + message: `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + }); } else if (es.logQueries === true) { - log( - `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".` - ); + addDeprecation({ + message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`, + }); } return settings; }, diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 356dad201ce95..daf7424b8f8bd 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -11,6 +11,7 @@ import { IHttpConfig, SslConfig, sslSchema } from '@kbn/server-http-tools'; import { hostname } from 'os'; import url from 'url'; +import { ServiceConfigDescriptor } from '../internal_types'; import { CspConfigType, CspConfig, ICspConfig } from '../csp'; import { ExternalUrlConfig, IExternalUrlConfig } from '../external_url'; @@ -20,141 +21,143 @@ const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); const match = (regex: RegExp, errorMsg: string) => (str: string) => regex.test(str) ? undefined : errorMsg; -// before update to make sure it's in sync with validation rules in Legacy -// https://github.com/elastic/kibana/blob/master/src/legacy/server/config/schema.js -export const config = { - path: 'server' as const, - schema: schema.object( - { - name: schema.string({ defaultValue: () => hostname() }), - autoListen: schema.boolean({ defaultValue: true }), - publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), - basePath: schema.maybe( - schema.string({ - validate: match(validBasePathRegex, "must start with a slash, don't end with one"), - }) - ), - cors: schema.object( - { - enabled: schema.boolean({ defaultValue: false }), - allowCredentials: schema.boolean({ defaultValue: false }), - allowOrigin: schema.oneOf( - [ - schema.arrayOf(hostURISchema, { minSize: 1 }), - schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), - ], - { - defaultValue: ['*'], - } - ), +const configSchema = schema.object( + { + name: schema.string({ defaultValue: () => hostname() }), + autoListen: schema.boolean({ defaultValue: true }), + publicBaseUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + basePath: schema.maybe( + schema.string({ + validate: match(validBasePathRegex, "must start with a slash, don't end with one"), + }) + ), + cors: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + allowCredentials: schema.boolean({ defaultValue: false }), + allowOrigin: schema.oneOf( + [ + schema.arrayOf(hostURISchema, { minSize: 1 }), + schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), + ], + { + defaultValue: ['*'], + } + ), + }, + { + validate(value) { + if (value.allowCredentials === true && value.allowOrigin.includes('*')) { + return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; + } }, - { - validate(value) { - if (value.allowCredentials === true && value.allowOrigin.includes('*')) { - return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; - } - }, + } + ), + customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { + defaultValue: {}, + }), + host: schema.string({ + defaultValue: 'localhost', + hostname: true, + validate(value) { + if (value === '0') { + return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; } + }, + }), + maxPayload: schema.byteSize({ + defaultValue: '1048576b', + }), + port: schema.number({ + defaultValue: 5601, + }), + rewriteBasePath: schema.boolean({ defaultValue: false }), + ssl: sslSchema, + keepaliveTimeout: schema.number({ + defaultValue: 120000, + }), + socketTimeout: schema.number({ + defaultValue: 120000, + }), + compression: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + referrerWhitelist: schema.maybe( + schema.arrayOf( + schema.string({ + hostname: true, + }), + { minSize: 1 } + ) + ), + }), + uuid: schema.maybe( + schema.string({ + validate: match(uuidRegexp, 'must be a valid uuid'), + }) + ), + xsrf: schema.object({ + disableProtection: schema.boolean({ defaultValue: false }), + allowlist: schema.arrayOf( + schema.string({ validate: match(/^\//, 'must start with a slash') }), + { defaultValue: [] } ), - customResponseHeaders: schema.recordOf(schema.string(), schema.any(), { - defaultValue: {}, - }), - host: schema.string({ - defaultValue: 'localhost', - hostname: true, + }), + requestId: schema.object( + { + allowFromAnyIp: schema.boolean({ defaultValue: false }), + ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), + }, + { validate(value) { - if (value === '0') { - return 'value 0 is not a valid hostname (use "0.0.0.0" to bind to all interfaces)'; + if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { + return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; } }, - }), - maxPayload: schema.byteSize({ - defaultValue: '1048576b', - }), - port: schema.number({ - defaultValue: 5601, - }), - rewriteBasePath: schema.boolean({ defaultValue: false }), - ssl: sslSchema, - keepaliveTimeout: schema.number({ - defaultValue: 120000, - }), - socketTimeout: schema.number({ - defaultValue: 120000, - }), - compression: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - referrerWhitelist: schema.maybe( - schema.arrayOf( - schema.string({ - hostname: true, - }), - { minSize: 1 } - ) - ), - }), - uuid: schema.maybe( - schema.string({ - validate: match(uuidRegexp, 'must be a valid uuid'), - }) - ), - xsrf: schema.object({ - disableProtection: schema.boolean({ defaultValue: false }), - allowlist: schema.arrayOf( - schema.string({ validate: match(/^\//, 'must start with a slash') }), - { defaultValue: [] } - ), - }), - requestId: schema.object( - { - allowFromAnyIp: schema.boolean({ defaultValue: false }), - ipAllowlist: schema.arrayOf(schema.ip(), { defaultValue: [] }), - }, - { - validate(value) { - if (value.allowFromAnyIp === true && value.ipAllowlist?.length > 0) { - return `allowFromAnyIp must be set to 'false' if any values are specified in ipAllowlist`; - } - }, + } + ), + }, + { + validate: (rawConfig) => { + if (!rawConfig.basePath && rawConfig.rewriteBasePath) { + return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + } + + if (rawConfig.publicBaseUrl) { + const parsedUrl = url.parse(rawConfig.publicBaseUrl); + if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { + return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; } - ), - }, - { - validate: (rawConfig) => { - if (!rawConfig.basePath && rawConfig.rewriteBasePath) { - return 'cannot use [rewriteBasePath] when [basePath] is not specified'; + if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { + return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; } + } - if (rawConfig.publicBaseUrl) { - const parsedUrl = url.parse(rawConfig.publicBaseUrl); - if (parsedUrl.query || parsedUrl.hash || parsedUrl.auth) { - return `[publicBaseUrl] may only contain a protocol, host, port, and pathname`; - } - if (parsedUrl.path !== (rawConfig.basePath ?? '/')) { - return `[publicBaseUrl] must contain the [basePath]: ${parsedUrl.path} !== ${rawConfig.basePath}`; - } - } + if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { + return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; + } - if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { - return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; - } + if ( + rawConfig.ssl.enabled && + rawConfig.ssl.redirectHttpFromPort !== undefined && + rawConfig.ssl.redirectHttpFromPort === rawConfig.port + ) { + return ( + 'Kibana does not accept http traffic to [port] when ssl is ' + + 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + + `cannot be configured to the same value. Both are [${rawConfig.port}].` + ); + } + }, + } +); - if ( - rawConfig.ssl.enabled && - rawConfig.ssl.redirectHttpFromPort !== undefined && - rawConfig.ssl.redirectHttpFromPort === rawConfig.port - ) { - return ( - 'Kibana does not accept http traffic to [port] when ssl is ' + - 'enabled (only https is allowed), so [ssl.redirectHttpFromPort] ' + - `cannot be configured to the same value. Both are [${rawConfig.port}].` - ); - } - }, - } - ), +export type HttpConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { + path: 'server' as const, + schema: configSchema, + deprecations: ({ rename }) => [rename('maxPayloadBytes', 'maxPayload')], }; -export type HttpConfigType = TypeOf; export class HttpConfig implements IHttpConfig { public name: string; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index af358caae8bfc..5433f0d3c3e31 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -12,8 +12,6 @@ import { legacyClusterClientInstanceMock, } from './core_service.test.mocks'; -import Boom from '@hapi/boom'; -import { Request } from '@hapi/hapi'; import { errors as esErrors } from 'elasticsearch'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; @@ -22,16 +20,6 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalElasticsearchServiceStart } from '../../elasticsearch'; -interface User { - id: string; - roles?: string[]; -} - -interface StorageData { - value: User; - expires: number; -} - const cookieOptions = { name: 'sid', encryptionKey: 'something_at_least_32_characters', @@ -197,172 +185,6 @@ describe('http service', () => { }); }); - describe('legacy server', () => { - describe('#registerAuth()', () => { - const sessionDurationMs = 1000; - - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - }, 30000); - - afterEach(async () => { - MockLegacyScopedClusterClient.mockClear(); - await root.shutdown(); - }); - - it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => { - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, res, toolkit) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return toolkit.authenticated({ state: user }); - } else { - return res.unauthorized(); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: () => 'ok from legacy server', - }); - - const response = await kbnTestServer.request - .get(root, legacyUrl) - .expect(200, 'ok from legacy server'); - - expect(response.header['set-cookie']).toHaveLength(1); - }); - - it('passes authHeaders as request headers to the legacy platform', async () => { - const token = 'Basic: name:password'; - const { http } = await root.setup(); - const sessionStorageFactory = await http.createCookieSessionStorageFactory( - cookieOptions - ); - http.registerAuth((req, res, toolkit) => { - if (req.headers.authorization) { - const user = { id: '42' }; - const sessionStorage = sessionStorageFactory.asScoped(req); - sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return toolkit.authenticated({ - state: user, - requestHeaders: { - authorization: token, - }, - }); - } else { - return res.unauthorized(); - } - }); - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: (req: Request) => ({ - authorization: req.headers.authorization, - custom: req.headers.custom, - }), - }); - - await kbnTestServer.request - .get(root, legacyUrl) - .set({ custom: 'custom-header' }) - .expect(200, { authorization: token, custom: 'custom-header' }); - }); - - it('attach security header to a successful response handled by Legacy platform', async () => { - const authResponseHeader = { - 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', - }; - const { http } = await root.setup(); - const { registerAuth } = http; - - registerAuth((req, res, toolkit) => { - return toolkit.authenticated({ responseHeaders: authResponseHeader }); - }); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: '/legacy', - handler: () => 'ok', - }); - - const response = await kbnTestServer.request.get(root, '/legacy').expect(200); - expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); - }); - - it('attach security header to an error response handled by Legacy platform', async () => { - const authResponseHeader = { - 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', - }; - const { http } = await root.setup(); - const { registerAuth } = http; - - registerAuth((req, res, toolkit) => { - return toolkit.authenticated({ responseHeaders: authResponseHeader }); - }); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: '/legacy', - handler: () => { - throw Boom.badRequest(); - }, - }); - - const response = await kbnTestServer.request.get(root, '/legacy').expect(400); - expect(response.header['www-authenticate']).toBe(authResponseHeader['www-authenticate']); - }); - }); - - describe('#basePath()', () => { - let root: ReturnType; - beforeEach(async () => { - root = kbnTestServer.createRoot({ plugins: { initialize: false } }); - }, 30000); - - afterEach(async () => await root.shutdown()); - it('basePath information for an incoming request is available in legacy server', async () => { - const reqBasePath = '/requests-specific-base-path'; - const { http } = await root.setup(); - http.registerOnPreRouting((req, res, toolkit) => { - http.basePath.set(req, reqBasePath); - return toolkit.next(); - }); - - await root.start(); - - const legacyUrl = '/legacy'; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyUrl, - handler: kbnServer.newPlatform.setup.core.http.basePath.get, - }); - - await kbnTestServer.request.get(root, legacyUrl).expect(200, reqBasePath); - }); - }); - }); describe('legacy elasticsearch client', () => { let root: ReturnType; beforeEach(async () => { diff --git a/src/core/server/i18n/get_kibana_translation_files.test.ts b/src/core/server/i18n/get_kibana_translation_files.test.ts index 7ca0fe0e79337..45e1a8dfec9cb 100644 --- a/src/core/server/i18n/get_kibana_translation_files.test.ts +++ b/src/core/server/i18n/get_kibana_translation_files.test.ts @@ -14,7 +14,7 @@ const mockGetTranslationPaths = getTranslationPaths as jest.Mock; jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn().mockResolvedValue([]), })); -jest.mock('../utils', () => ({ +jest.mock('@kbn/utils', () => ({ fromRoot: jest.fn().mockImplementation((path: string) => path), })); diff --git a/src/core/server/i18n/get_kibana_translation_files.ts b/src/core/server/i18n/get_kibana_translation_files.ts index 7b5ada2a25f4f..4e7ee718113ce 100644 --- a/src/core/server/i18n/get_kibana_translation_files.ts +++ b/src/core/server/i18n/get_kibana_translation_files.ts @@ -7,7 +7,7 @@ */ import { basename } from 'path'; -import { fromRoot } from '../utils'; +import { fromRoot } from '@kbn/utils'; import { getTranslationPaths } from './get_translation_paths'; export const getKibanaTranslationFiles = async ( diff --git a/src/core/server/i18n/get_translation_paths.test.ts b/src/core/server/i18n/get_translation_paths.test.ts index 9094b008be739..3e9d68c16d30e 100644 --- a/src/core/server/i18n/get_translation_paths.test.ts +++ b/src/core/server/i18n/get_translation_paths.test.ts @@ -23,14 +23,14 @@ describe('getTranslationPaths', () => { getTranslationPaths({ cwd: '/some/cwd', nested: false }); expect(globbyMock).toHaveBeenCalledTimes(1); - expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd' }); + expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd', dot: true }); globbyMock.mockClear(); await getTranslationPaths({ cwd: '/other/cwd', nested: true }); expect(globbyMock).toHaveBeenCalledTimes(1); - expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd' }); + expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd', dot: true }); }); it('calls `readFile` for each entry returned by `globby`', async () => { diff --git a/src/core/server/i18n/get_translation_paths.ts b/src/core/server/i18n/get_translation_paths.ts index 93b10da73dcc7..8897786252d40 100644 --- a/src/core/server/i18n/get_translation_paths.ts +++ b/src/core/server/i18n/get_translation_paths.ts @@ -18,7 +18,7 @@ const I18N_RC = '.i18nrc.json'; export async function getTranslationPaths({ cwd, nested }: { cwd: string; nested: boolean }) { const glob = nested ? `*/${I18N_RC}` : I18N_RC; - const entries = await globby(glob, { cwd }); + const entries = await globby(glob, { cwd, dot: true }); const translationPaths: string[] = []; for (const entry of entries) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 788c179501a80..2c6fa74cb54a0 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -57,7 +57,7 @@ import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; - +import { DeprecationsServiceSetup } from './deprecations'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected import { @@ -88,8 +88,8 @@ export type { ConfigService, ConfigDeprecation, ConfigDeprecationProvider, - ConfigDeprecationLogger, ConfigDeprecationFactory, + AddConfigDeprecation, EnvironmentMode, PackageInfo, } from './config'; @@ -381,6 +381,12 @@ export type { } from './metrics'; export type { I18nServiceSetup } from './i18n'; +export type { + DeprecationsDetails, + RegisterDeprecationsConfig, + GetDeprecationsContext, + DeprecationsServiceSetup, +} from './deprecations'; export type { AppCategory } from '../types'; export { DEFAULT_APP_CATEGORIES } from '../utils'; @@ -400,8 +406,6 @@ export type { SavedObjectsMigrationVersion, } from './types'; -export type { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig } from './legacy'; - export { ServiceStatusLevels } from './status'; export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status'; @@ -481,6 +485,8 @@ export interface CoreSetup; } diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 28194a7d0dc3a..34193f8d0c35e 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -29,6 +29,7 @@ import { InternalStatusServiceSetup } from './status'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; import { I18nServiceSetup } from './i18n'; +import { InternalDeprecationsServiceSetup } from './deprecations'; /** @internal */ export interface InternalCoreSetup { @@ -45,6 +46,7 @@ export interface InternalCoreSetup { httpResources: InternalHttpResourcesSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; + deprecations: InternalDeprecationsServiceSetup; } /** diff --git a/src/core/server/kibana_config.test.ts b/src/core/server/kibana_config.test.ts index 1c2b268156531..1acdff9dd78e6 100644 --- a/src/core/server/kibana_config.test.ts +++ b/src/core/server/kibana_config.test.ts @@ -22,7 +22,7 @@ const applyKibanaDeprecations = (settings: Record = {}) => { deprecation, path: CONFIG_PATH, })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index d0ff18b381179..97783a7657db5 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -12,12 +12,13 @@ import { ConfigDeprecationProvider } from '@kbn/config'; export type KibanaConfigType = TypeOf; const deprecations: ConfigDeprecationProvider = () => [ - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const kibana = settings[fromPath]; if (kibana?.index) { - log( - `"kibana.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details` - ); + addDeprecation({ + message: `"kibana.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', + }); } return settings; }, diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap deleted file mode 100644 index 69b7f9fc78315..0000000000000 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` -Array [ - Array [ - Object { - "logging": Object { - "verbose": true, - }, - "path": Object {}, - }, - ], -] -`; - -exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` -Array [ - Array [ - Object { - "logging": Object { - "verbose": true, - }, - "path": Object {}, - }, - ], -] -`; diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts deleted file mode 100644 index febf91625378d..0000000000000 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 { ensureValidConfiguration } from './ensure_valid_configuration'; -import { getUnusedConfigKeys } from './get_unused_config_keys'; -import { configServiceMock } from '../../config/mocks'; - -jest.mock('./get_unused_config_keys'); - -describe('ensureValidConfiguration', () => { - let configService: ReturnType; - - beforeEach(() => { - jest.clearAllMocks(); - configService = configServiceMock.create(); - configService.getUsedPaths.mockReturnValue(Promise.resolve(['core', 'elastic'])); - - (getUnusedConfigKeys as any).mockImplementation(() => []); - }); - - it('calls getUnusedConfigKeys with correct parameters', async () => { - await ensureValidConfiguration( - configService as any, - { - settings: 'settings', - legacyConfig: 'pluginExtendedConfig', - } as any - ); - expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); - expect(getUnusedConfigKeys).toHaveBeenCalledWith({ - coreHandledConfigPaths: ['core', 'elastic'], - settings: 'settings', - legacyConfig: 'pluginExtendedConfig', - }); - }); - - it('returns normally when there is no unused keys', async () => { - await expect( - ensureValidConfiguration(configService as any, {} as any) - ).resolves.toBeUndefined(); - - expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); - }); - - it('throws when there are some unused keys', async () => { - (getUnusedConfigKeys as any).mockImplementation(() => ['some.key', 'some.other.key']); - - await expect( - ensureValidConfiguration(configService as any, {} as any) - ).rejects.toMatchInlineSnapshot( - `[Error: Unknown configuration key(s): "some.key", "some.other.key". Check for spelling errors and ensure that expected plugins are installed.]` - ); - }); -}); diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts deleted file mode 100644 index 86b4e0aeeea59..0000000000000 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyConfig, LegacyVars } from '../types'; -import { getUnusedConfigKeys } from './get_unused_config_keys'; - -describe('getUnusedConfigKeys', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - const getConfig = (values: LegacyVars = {}): LegacyConfig => - ({ - get: () => values as any, - } as LegacyConfig); - - describe('not using core or plugin specs', () => { - it('should return an empty list for empty parameters', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: {}, - legacyConfig: getConfig(), - }) - ).toEqual([]); - }); - - it('returns empty list when config and settings have the same properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - alsoInBoth: 'someValue', - }, - legacyConfig: getConfig({ - presentInBoth: true, - alsoInBoth: 'someValue', - }), - }) - ).toEqual([]); - }); - - it('returns empty list when config has entries not present in settings', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - }, - legacyConfig: getConfig({ - presentInBoth: true, - onlyInConfig: 'someValue', - }), - }) - ).toEqual([]); - }); - - it('returns the list of properties from settings not present in config', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - presentInBoth: true, - onlyInSetting: 'value', - }, - legacyConfig: getConfig({ - presentInBoth: true, - }), - }) - ).toEqual(['onlyInSetting']); - }); - - it('correctly handle nested properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - elasticsearch: { - username: 'foo', - password: 'bar', - }, - }, - legacyConfig: getConfig({ - elasticsearch: { - username: 'foo', - onlyInConfig: 'default', - }, - }), - }) - ).toEqual(['elasticsearch.password']); - }); - - it('correctly handle "env" specific case', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - env: 'development', - }, - legacyConfig: getConfig({ - env: { - name: 'development', - }, - }), - }) - ).toEqual([]); - }); - - it('correctly handle array properties', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - settings: { - prop: ['a', 'b', 'c'], - }, - legacyConfig: getConfig({ - prop: ['a'], - }), - }) - ).toEqual([]); - }); - }); - - it('ignores properties managed by the new platform', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: ['core', 'foo.bar'], - settings: { - core: { - prop: 'value', - }, - foo: { - bar: true, - dolly: true, - }, - }, - legacyConfig: getConfig({}), - }) - ).toEqual(['foo.dolly']); - }); - - it('handles array values', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: ['core', 'array'], - settings: { - core: { - prop: 'value', - array: [1, 2, 3], - }, - array: ['some', 'values'], - }, - legacyConfig: getConfig({}), - }) - ).toEqual([]); - }); -}); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts deleted file mode 100644 index a2da6dc97225e..0000000000000 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { difference } from 'lodash'; -import { getFlattenedObject } from '@kbn/std'; -import { hasConfigPathIntersection } from '../../config'; -import { LegacyConfig, LegacyVars } from '../types'; - -const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object)); - -export async function getUnusedConfigKeys({ - coreHandledConfigPaths, - settings, - legacyConfig, -}: { - coreHandledConfigPaths: string[]; - settings: LegacyVars; - legacyConfig: LegacyConfig; -}) { - const inputKeys = getFlattenedKeys(settings); - const appliedKeys = getFlattenedKeys(legacyConfig.get()); - - if (inputKeys.includes('env')) { - // env is a special case key, see https://github.com/elastic/kibana/blob/848bf17b/src/legacy/server/config/config.js#L74 - // where it is deleted from the settings before being injected into the schema via context and - // then renamed to `env.name` https://github.com/elastic/kibana/blob/848bf17/src/legacy/server/config/schema.js#L17 - inputKeys[inputKeys.indexOf('env')] = 'env.name'; - } - - // Filter out keys that are marked as used in the core (e.g. by new core plugins). - return difference(inputKeys, appliedKeys).filter( - (unusedConfigKey) => - !coreHandledConfigPaths.some((usedInCoreConfigKey) => - hasConfigPathIntersection(unusedConfigKey, usedInCoreConfigKey) - ) - ); -} diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts index 8614265e4375d..39ffef501a9ec 100644 --- a/src/core/server/legacy/index.ts +++ b/src/core/server/legacy/index.ts @@ -6,16 +6,6 @@ * Side Public License, v 1. */ -/** @internal */ -export { ensureValidConfiguration } from './config'; /** @internal */ export type { ILegacyService } from './legacy_service'; export { LegacyService } from './legacy_service'; -/** @internal */ -export type { - LegacyVars, - LegacyConfig, - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyServiceSetupConfig, -} from './types'; diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts deleted file mode 100644 index 715749c6ef0cb..0000000000000 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as kbnTestServer from '../../../test_helpers/kbn_server'; - -describe('legacy service', () => { - describe('http server', () => { - let root: ReturnType; - beforeEach(() => { - root = kbnTestServer.createRoot({ - migrations: { skip: true }, - plugins: { initialize: false }, - }); - }, 30000); - - afterEach(async () => await root.shutdown()); - - it("handles http request in Legacy platform if New platform doesn't handle it", async () => { - const { http } = await root.setup(); - const rootUrl = '/route'; - const router = http.createRouter(rootUrl); - router.get({ path: '/new-platform', validate: false }, (context, req, res) => - res.ok({ body: 'from-new-platform' }) - ); - - await root.start(); - - const legacyPlatformUrl = `${rootUrl}/legacy-platform`; - const kbnServer = kbnTestServer.getKbnServer(root); - kbnServer.server.route({ - method: 'GET', - path: legacyPlatformUrl, - handler: () => 'ok from legacy server', - }); - - await kbnTestServer.request.get(root, '/route/new-platform').expect(200, 'from-new-platform'); - - await kbnTestServer.request.get(root, legacyPlatformUrl).expect(200, 'ok from legacy server'); - }); - it('throws error if Legacy and New platforms register handler for the same route', async () => { - const { http } = await root.setup(); - const rootUrl = '/route'; - const router = http.createRouter(rootUrl); - router.get({ path: '', validate: false }, (context, req, res) => - res.ok({ body: 'from-new-platform' }) - ); - - await root.start(); - - const kbnServer = kbnTestServer.getKbnServer(root); - expect(() => - kbnServer.server.route({ - method: 'GET', - path: rootUrl, - handler: () => 'ok from legacy server', - }) - ).toThrowErrorMatchingInlineSnapshot(`"New route /route conflicts with existing /route"`); - }); - }); -}); diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index 1f4c308be0107..0d72318a630e0 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -8,26 +8,14 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { LegacyService } from './legacy_service'; -import { LegacyConfig, LegacyServiceSetupDeps } from './types'; -type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; +type LegacyServiceMock = jest.Mocked>; const createLegacyServiceMock = (): LegacyServiceMock => ({ - legacyId: Symbol(), - setupLegacyConfig: jest.fn(), setup: jest.fn(), - start: jest.fn(), stop: jest.fn(), }); -const createLegacyConfigMock = (): jest.Mocked => ({ - get: jest.fn(), - has: jest.fn(), - set: jest.fn(), -}); - export const legacyServiceMock = { create: createLegacyServiceMock, - createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), - createLegacyConfig: createLegacyConfigMock, }; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts new file mode 100644 index 0000000000000..506f0fd6f96d3 --- /dev/null +++ b/src/core/server/legacy/legacy_service.test.mocks.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const reconfigureLoggingMock = jest.fn(); +export const setupLoggingMock = jest.fn(); +export const setupLoggingRotateMock = jest.fn(); + +jest.doMock('@kbn/legacy-logging', () => ({ + ...(jest.requireActual('@kbn/legacy-logging') as any), + reconfigureLogging: reconfigureLoggingMock, + setupLogging: setupLoggingMock, + setupLoggingRotate: setupLoggingRotateMock, +})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index db36bd73602c4..6b20bd7434baf 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -6,34 +6,22 @@ * Side Public License, v 1. */ -jest.mock('../../../legacy/server/kbn_server'); - -import { BehaviorSubject, throwError } from 'rxjs'; +import { + setupLoggingMock, + setupLoggingRotateMock, + reconfigureLoggingMock, +} from './legacy_service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import moment from 'moment'; import { REPO_ROOT } from '@kbn/dev-utils'; -import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; -import { DiscoveredPlugin } from '../plugins'; import { getEnvOptions, configServiceMock } from '../config/mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; -import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; -import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; -import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; -import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; -import { environmentServiceMock } from '../environment/environment_service.mock'; -import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; -import { LegacyService } from './legacy_service'; -import { coreMock } from '../mocks'; -import { statusServiceMock } from '../status/status_service.mock'; -import { loggingServiceMock } from '../logging/logging_service.mock'; -import { metricsServiceMock } from '../metrics/metrics_service.mock'; -import { i18nServiceMock } from '../i18n/i18n_service.mock'; - -const MockKbnServer: jest.Mock = KbnServer as any; +import { LegacyService, LegacyServiceSetupDeps } from './legacy_service'; let coreId: symbol; let env: Env; @@ -41,69 +29,16 @@ let config$: BehaviorSubject; let setupDeps: LegacyServiceSetupDeps; -let startDeps: LegacyServiceStartDeps; - const logger = loggingSystemMock.create(); let configService: ReturnType; -let environmentSetup: ReturnType; beforeEach(() => { coreId = Symbol(); env = Env.createDefault(REPO_ROOT, getEnvOptions()); configService = configServiceMock.create(); - environmentSetup = environmentServiceMock.createSetupContract(); - - MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); - MockKbnServer.prototype.listen = jest.fn(); setupDeps = { - core: { - capabilities: capabilitiesServiceMock.createSetupContract(), - context: contextServiceMock.createSetupContract(), - elasticsearch: { legacy: {} } as any, - i18n: i18nServiceMock.createSetupContract(), - uiSettings: uiSettingsServiceMock.createSetupContract(), - http: { - ...httpServiceMock.createInternalSetupContract(), - auth: { - getAuthHeaders: () => undefined, - } as any, - }, - httpResources: httpResourcesMock.createSetupContract(), - savedObjects: savedObjectsServiceMock.createInternalSetupContract(), - plugins: { - initialized: true, - contracts: new Map([['plugin-id', 'plugin-value']]), - }, - rendering: renderingServiceMock, - environment: environmentSetup, - status: statusServiceMock.createInternalSetupContract(), - logging: loggingServiceMock.createInternalSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), - }, - plugins: { 'plugin-id': 'plugin-value' }, - uiPlugins: { - public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([ - [ - 'plugin-id', - { - requiredBundles: [], - publicTargetDir: 'path/to/target/public', - publicAssetsDir: '/plugins/name/assets/', - }, - ], - ]), - browserConfigs: new Map(), - }, - }; - - startDeps = { - core: { - ...coreMock.createInternalStart(), - plugins: { contracts: new Map() }, - }, - plugins: {}, + http: httpServiceMock.createInternalSetupContract(), }; config$ = new BehaviorSubject( @@ -114,98 +49,78 @@ beforeEach(() => { ); configService.getConfig$.mockReturnValue(config$); - configService.getUsedPaths.mockResolvedValue(['foo.bar']); }); afterEach(() => { jest.clearAllMocks(); + setupLoggingMock.mockReset(); + setupLoggingRotateMock.mockReset(); + reconfigureLoggingMock.mockReset(); }); -describe('once LegacyService is set up with connection info', () => { - test('creates legacy kbnServer and calls `listen`.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService, +describe('#setup', () => { + it('initializes legacy logging', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) - ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual( - expect.objectContaining({ - path: expect.objectContaining({ autoListen: true }), - server: expect.objectContaining({ autoListen: true }), - }) - ); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); - expect(mockKbnServer.close).not.toHaveBeenCalled(); - }); - - test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); - const legacyService = new LegacyService({ coreId, env, logger, configService: configService as any, }); - await legacyService.setupLegacyConfig(); + await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: { autoListen: false }, server: { autoListen: true } }, - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) + expect(setupLoggingMock).toHaveBeenCalledTimes(1); + expect(setupLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - const legacyConfig = MockKbnServer.mock.calls[0][1].get(); - expect(legacyConfig.path.autoListen).toBe(false); - expect(legacyConfig.server.autoListen).toBe(true); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); - expect(mockKbnServer.listen).not.toHaveBeenCalled(); - expect(mockKbnServer.close).not.toHaveBeenCalled(); + expect(setupLoggingRotateMock).toHaveBeenCalledTimes(1); + expect(setupLoggingRotateMock).toHaveBeenCalledWith(setupDeps.http.server, loggingConfig); }); - test('creates legacy kbnServer and closes it if `listen` fails.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + it('reloads the logging config when the config changes', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"something failed"` - ); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.listen).toHaveBeenCalled(); - expect(mockKbnServer.close).toHaveBeenCalled(); - }); - - test('throws if fails to retrieve initial config.', async () => { - configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); const legacyService = new LegacyService({ coreId, env, @@ -213,150 +128,70 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await expect(legacyService.setupLegacyConfig()).rejects.toThrowErrorMatchingInlineSnapshot( - `"something failed"` - ); - await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()"` - ); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); - - expect(MockKbnServer).not.toHaveBeenCalled(); - }); - - test('reconfigures logging configuration if new config is received.', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); - expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( - `applyLoggingConfiguration params` + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - }); - test('logs error if re-configuring fails.', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + loggingConfig$.next({ + foo: 'changed', }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([]); + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(2); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + { foo: 'changed' }, + opsConfig.interval.asMilliseconds() + ); + }); - const configError = new Error('something went wrong'); - mockKbnServer.applyLoggingConfiguration.mockImplementation(() => { - throw configError; + it('stops reloading logging config once the service is stopped', async () => { + const opsConfig = { + interval: moment.duration(5, 'second'), + }; + const opsConfig$ = new BehaviorSubject(opsConfig); + + const loggingConfig = { + foo: 'bar', + }; + const loggingConfig$ = new BehaviorSubject(loggingConfig); + + configService.atPath.mockImplementation((path) => { + if (path === 'ops') { + return opsConfig$; + } + if (path === 'logging') { + return loggingConfig$; + } + return new BehaviorSubject({}); }); - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); - - expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]); - }); - - test('logs error if config service fails.', async () => { const legacyService = new LegacyService({ coreId, env, logger, configService: configService as any, }); - await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [mockKbnServer] = MockKbnServer.mock.instances; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([]); - - const configError = new Error('something went wrong'); - config$.error(configError); - - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]); - }); -}); -describe('once LegacyService is set up without connection info', () => { - let legacyService: LegacyService; - beforeEach(async () => { - legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); - await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - }); - test('creates legacy kbnServer with `autoListen: false`.', () => { - expect(MockKbnServer).toHaveBeenCalledTimes(1); - expect(MockKbnServer).toHaveBeenCalledWith( - { path: {}, server: { autoListen: true } }, - expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object) - ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual( - expect.objectContaining({ - server: expect.objectContaining({ autoListen: true }), - }) + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); + expect(reconfigureLoggingMock).toHaveBeenCalledWith( + setupDeps.http.server, + loggingConfig, + opsConfig.interval.asMilliseconds() ); - }); - - test('reconfigures logging configuration if new config is received.', async () => { - const [mockKbnServer] = MockKbnServer.mock.instances as Array>; - expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); - config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + await legacyService.stop(); - expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( - `applyLoggingConfiguration params` - ); - }); -}); - -describe('start', () => { - test('Cannot start without setup phase', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + loggingConfig$.next({ + foo: 'changed', }); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); - }); -}); -test('Sets the server.uuid property on the legacy configuration', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, + expect(reconfigureLoggingMock).toHaveBeenCalledTimes(1); }); - - environmentSetup.instanceUuid = 'UUID_FROM_SERVICE'; - - const { legacyConfig } = await legacyService.setupLegacyConfig(); - await legacyService.setup(setupDeps); - - expect(legacyConfig.get('server.uuid')).toBe('UUID_FROM_SERVICE'); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index f7abe942d0009..1d5343ff5311d 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -6,141 +6,61 @@ * Side Public License, v 1. */ -import { combineLatest, ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { first, map, publishReplay, tap } from 'rxjs/operators'; +import { combineLatest, Observable, Subscription } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { Server } from '@hapi/hapi'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PathConfigType } from '@kbn/utils'; +import { + reconfigureLogging, + setupLogging, + setupLoggingRotate, + LegacyLoggingConfig, +} from '@kbn/legacy-logging'; -import type { RequestHandlerContext } from 'src/core/server'; -// @ts-expect-error legacy config class -import { Config as LegacyConfigClass } from '../../../legacy/server/config'; -import { CoreService } from '../../types'; -import { Config } from '../config'; import { CoreContext } from '../core_context'; -import { CspConfigType, config as cspConfig } from '../csp'; -import { - HttpConfig, - HttpConfigType, - config as httpConfig, - IRouter, - RequestHandlerContextProvider, -} from '../http'; +import { config as loggingConfig } from '../logging'; +import { opsConfig, OpsConfigType } from '../metrics'; import { Logger } from '../logging'; -import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; -import { ExternalUrlConfigType, config as externalUrlConfig } from '../external_url'; -import { CoreSetup, CoreStart } from '..'; - -interface LegacyKbnServer { - applyLoggingConfiguration: (settings: Readonly) => void; - listen: () => Promise; - ready: () => Promise; - close: () => Promise; -} +import { InternalHttpServiceSetup } from '../http'; -function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) { - const rawConfig = config.toRaw(); - - // Elasticsearch config is solely handled by the core and legacy platform - // shouldn't have direct access to it. - if (rawConfig.elasticsearch !== undefined) { - delete rawConfig.elasticsearch; - } - - return { - ...rawConfig, - // We rely heavily in the default value of 'path.data' in the legacy world and, - // since it has been moved to NP, it won't show up in RawConfig. - path: pathConfig, - }; +export interface LegacyServiceSetupDeps { + http: InternalHttpServiceSetup; } /** @internal */ export type ILegacyService = PublicMethodsOf; /** @internal */ -export class LegacyService implements CoreService { - /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ - public readonly legacyId = Symbol(); +export class LegacyService { private readonly log: Logger; - private readonly httpConfig$: Observable; - private kbnServer?: LegacyKbnServer; + private readonly opsConfig$: Observable; + private readonly legacyLoggingConfig$: Observable; private configSubscription?: Subscription; - private setupDeps?: LegacyServiceSetupDeps; - private update$?: ConnectableObservable<[Config, PathConfigType]>; - private legacyRawConfig?: LegacyConfig; - private settings?: LegacyVars; - constructor(private readonly coreContext: CoreContext) { + constructor(coreContext: CoreContext) { const { logger, configService } = coreContext; this.log = logger.get('legacy-service'); - this.httpConfig$ = combineLatest( - configService.atPath(httpConfig.path), - configService.atPath(cspConfig.path), - configService.atPath(externalUrlConfig.path) - ).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); - } - - public async setupLegacyConfig() { - this.update$ = combineLatest([ - this.coreContext.configService.getConfig$(), - this.coreContext.configService.atPath('path'), - ]).pipe( - tap(([config, pathConfig]) => { - if (this.kbnServer !== undefined) { - this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig)); - } - }), - tap({ error: (err) => this.log.error(err) }), - publishReplay(1) - ) as ConnectableObservable<[Config, PathConfigType]>; - - this.configSubscription = this.update$.connect(); - - this.settings = await this.update$ - .pipe( - first(), - map(([config, pathConfig]) => getLegacyRawConfig(config, pathConfig)) - ) - .toPromise(); - - this.legacyRawConfig = LegacyConfigClass.withDefaultSchema(this.settings); - - return { - settings: this.settings, - legacyConfig: this.legacyRawConfig!, - }; + this.legacyLoggingConfig$ = configService.atPath(loggingConfig.path); + this.opsConfig$ = configService.atPath(opsConfig.path); } public async setup(setupDeps: LegacyServiceSetupDeps) { this.log.debug('setting up legacy service'); - - if (!this.legacyRawConfig) { - throw new Error( - 'Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()' - ); - } - - // propagate the instance uuid to the legacy config, as it was the legacy way to access it. - this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid); - - this.setupDeps = setupDeps; + await this.setupLegacyLogging(setupDeps.http.server); } - public async start(startDeps: LegacyServiceStartDeps) { - const { setupDeps } = this; - - if (!setupDeps || !this.legacyRawConfig) { - throw new Error('Legacy service is not setup yet.'); - } + private async setupLegacyLogging(server: Server) { + const legacyLoggingConfig = await this.legacyLoggingConfig$.pipe(first()).toPromise(); + const currentOpsConfig = await this.opsConfig$.pipe(first()).toPromise(); - this.log.debug('starting legacy service'); + await setupLogging(server, legacyLoggingConfig, currentOpsConfig.interval.asMilliseconds()); + await setupLoggingRotate(server, legacyLoggingConfig); - this.kbnServer = await this.createKbnServer( - this.settings!, - this.legacyRawConfig!, - setupDeps, - startDeps + this.configSubscription = combineLatest([this.legacyLoggingConfig$, this.opsConfig$]).subscribe( + ([newLoggingConfig, newOpsConfig]) => { + reconfigureLogging(server, newLoggingConfig, newOpsConfig.interval.asMilliseconds()); + } ); } @@ -151,151 +71,5 @@ export class LegacyService implements CoreService { this.configSubscription.unsubscribe(); this.configSubscription = undefined; } - - if (this.kbnServer !== undefined) { - await this.kbnServer.close(); - this.kbnServer = undefined; - } - } - - private async createKbnServer( - settings: LegacyVars, - config: LegacyConfig, - setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps - ) { - const coreStart: CoreStart = { - capabilities: startDeps.core.capabilities, - elasticsearch: startDeps.core.elasticsearch, - http: { - auth: startDeps.core.http.auth, - basePath: startDeps.core.http.basePath, - getServerInfo: startDeps.core.http.getServerInfo, - }, - savedObjects: { - getScopedClient: startDeps.core.savedObjects.getScopedClient, - createScopedRepository: startDeps.core.savedObjects.createScopedRepository, - createInternalRepository: startDeps.core.savedObjects.createInternalRepository, - createSerializer: startDeps.core.savedObjects.createSerializer, - createExporter: startDeps.core.savedObjects.createExporter, - createImporter: startDeps.core.savedObjects.createImporter, - getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, - }, - metrics: { - collectionInterval: startDeps.core.metrics.collectionInterval, - getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, - }, - uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, - coreUsageData: { - getCoreUsageData: () => { - throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy'); - }, - }, - }; - - const router = setupDeps.core.http.createRouter('', this.legacyId); - const coreSetup: CoreSetup = { - capabilities: setupDeps.core.capabilities, - context: setupDeps.core.context, - elasticsearch: { - legacy: setupDeps.core.elasticsearch.legacy, - }, - http: { - createCookieSessionStorageFactory: setupDeps.core.http.createCookieSessionStorageFactory, - registerRouteHandlerContext: < - Context extends RequestHandlerContext, - ContextName extends keyof Context - >( - contextName: ContextName, - provider: RequestHandlerContextProvider - ) => setupDeps.core.http.registerRouteHandlerContext(this.legacyId, contextName, provider), - createRouter: () => - router as IRouter, - resources: setupDeps.core.httpResources.createRegistrar(router), - registerOnPreRouting: setupDeps.core.http.registerOnPreRouting, - registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, - registerAuth: setupDeps.core.http.registerAuth, - registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, - registerOnPreResponse: setupDeps.core.http.registerOnPreResponse, - basePath: setupDeps.core.http.basePath, - auth: { - get: setupDeps.core.http.auth.get, - isAuthenticated: setupDeps.core.http.auth.isAuthenticated, - }, - csp: setupDeps.core.http.csp, - getServerInfo: setupDeps.core.http.getServerInfo, - }, - i18n: setupDeps.core.i18n, - logging: { - configure: (config$) => setupDeps.core.logging.configure([], config$), - }, - metrics: { - collectionInterval: setupDeps.core.metrics.collectionInterval, - getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, - }, - savedObjects: { - setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, - addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, - registerType: setupDeps.core.savedObjects.registerType, - }, - status: { - isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, - core$: setupDeps.core.status.core$, - overall$: setupDeps.core.status.overall$, - set: () => { - throw new Error(`core.status.set is unsupported in legacy`); - }, - // @ts-expect-error - get dependencies$() { - throw new Error(`core.status.dependencies$ is unsupported in legacy`); - }, - // @ts-expect-error - get derivedStatus$() { - throw new Error(`core.status.derivedStatus$ is unsupported in legacy`); - }, - }, - uiSettings: { - register: setupDeps.core.uiSettings.register, - }, - getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), - }; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer(settings, config, { - env: { - mode: this.coreContext.env.mode, - packageInfo: this.coreContext.env.packageInfo, - }, - setupDeps: { - core: coreSetup, - plugins: setupDeps.plugins, - }, - startDeps: { - core: coreStart, - plugins: startDeps.plugins, - }, - __internals: { - hapiServer: setupDeps.core.http.server, - uiPlugins: setupDeps.uiPlugins, - rendering: setupDeps.core.rendering, - }, - logger: this.coreContext.logger, - }); - - const { autoListen } = await this.httpConfig$.pipe(first()).toPromise(); - - if (autoListen) { - try { - await kbnServer.listen(); - } catch (err) { - await kbnServer.close(); - throw err; - } - } else { - await kbnServer.ready(); - } - - return kbnServer; } } diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index a89441a5671b5..7e02d00c7b234 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -9,11 +9,10 @@ import { schema } from '@kbn/config-schema'; import { LegacyLoggingServer } from '@kbn/legacy-logging'; import { DisposableAppender, LogRecord } from '@kbn/logging'; -import { LegacyVars } from '../../types'; export interface LegacyAppenderConfig { type: 'legacy-appender'; - legacyLoggingConfig?: any; + legacyLoggingConfig?: Record; } /** @@ -23,7 +22,7 @@ export interface LegacyAppenderConfig { export class LegacyAppender implements DisposableAppender { public static configSchema = schema.object({ type: schema.literal('legacy-appender'), - legacyLoggingConfig: schema.any(), + legacyLoggingConfig: schema.recordOf(schema.string(), schema.any()), }); /** @@ -34,7 +33,7 @@ export class LegacyAppender implements DisposableAppender { private readonly loggingServer: LegacyLoggingServer; - constructor(legacyLoggingConfig: Readonly) { + constructor(legacyLoggingConfig: any) { this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig); } diff --git a/src/core/server/legacy/merge_vars.test.ts b/src/core/server/legacy/merge_vars.test.ts deleted file mode 100644 index e4268a52aa8ca..0000000000000 --- a/src/core/server/legacy/merge_vars.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { mergeVars } from './merge_vars'; - -describe('mergeVars', () => { - it('merges two objects together', () => { - const first = { - otherName: 'value', - otherCanFoo: true, - otherNested: { - otherAnotherVariable: 'ok', - }, - }; - const second = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - otherName: 'value', - otherCanFoo: true, - otherNested: { - otherAnotherVariable: 'ok', - }, - }); - }); - - it('does not mutate the source objects', () => { - const first = { - var1: 'first', - }; - const second = { - var1: 'second', - var2: 'second', - }; - const third = { - var1: 'third', - var2: 'third', - var3: 'third', - }; - const fourth = { - var1: 'fourth', - var2: 'fourth', - var3: 'fourth', - var4: 'fourth', - }; - - mergeVars(first, second, third, fourth); - - expect(first).toEqual({ var1: 'first' }); - expect(second).toEqual({ var1: 'second', var2: 'second' }); - expect(third).toEqual({ var1: 'third', var2: 'third', var3: 'third' }); - expect(fourth).toEqual({ var1: 'fourth', var2: 'fourth', var3: 'fourth', var4: 'fourth' }); - }); - - it('merges multiple objects together with precedence increasing from left-to-right', () => { - const first = { - var1: 'first', - var2: 'first', - var3: 'first', - var4: 'first', - }; - const second = { - var1: 'second', - var2: 'second', - var3: 'second', - }; - const third = { - var1: 'third', - var2: 'third', - }; - const fourth = { - var1: 'fourth', - }; - - expect(mergeVars(first, second, third, fourth)).toEqual({ - var1: 'fourth', - var2: 'third', - var3: 'second', - var4: 'first', - }); - }); - - it('overwrites the original variable value if a duplicate entry is found', () => { - const first = { - nested: { - otherAnotherVariable: 'ok', - }, - }; - const second = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }); - }); - - it('combines entries within "uiCapabilities"', () => { - const first = { - uiCapabilities: { - firstCapability: 'ok', - sharedCapability: 'shared', - }, - }; - const second = { - name: 'value', - canFoo: true, - uiCapabilities: { - secondCapability: 'ok', - }, - }; - const third = { - name: 'value', - canFoo: true, - uiCapabilities: { - thirdCapability: 'ok', - sharedCapability: 'blocked', - }, - }; - - expect(mergeVars(first, second, third)).toEqual({ - name: 'value', - canFoo: true, - uiCapabilities: { - firstCapability: 'ok', - secondCapability: 'ok', - thirdCapability: 'ok', - sharedCapability: 'blocked', - }, - }); - }); - - it('does not deeply combine entries within "uiCapabilities"', () => { - const first = { - uiCapabilities: { - firstCapability: 'ok', - nestedCapability: { - otherNestedProp: 'otherNestedValue', - }, - }, - }; - const second = { - name: 'value', - canFoo: true, - uiCapabilities: { - secondCapability: 'ok', - nestedCapability: { - nestedProp: 'nestedValue', - }, - }, - }; - - expect(mergeVars(first, second)).toEqual({ - name: 'value', - canFoo: true, - uiCapabilities: { - firstCapability: 'ok', - secondCapability: 'ok', - nestedCapability: { - nestedProp: 'nestedValue', - }, - }, - }); - }); -}); diff --git a/src/core/server/legacy/merge_vars.ts b/src/core/server/legacy/merge_vars.ts deleted file mode 100644 index cd2cbb0d8cde2..0000000000000 --- a/src/core/server/legacy/merge_vars.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { LegacyVars } from './types'; - -const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities']; - -export function mergeVars(...sources: LegacyVars[]): LegacyVars { - return Object.assign( - {}, - ...sources, - ...ELIGIBLE_FLAT_MERGE_KEYS.flatMap((key) => - sources.some((source) => key in source) - ? [{ [key]: Object.assign({}, ...sources.map((source) => source[key] || {})) }] - : [] - ) - ); -} diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts deleted file mode 100644 index 9f562d3da3029..0000000000000 --- a/src/core/server/legacy/types.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; -import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; -import { InternalRenderingServiceSetup } from '../rendering'; - -/** - * @internal - * @deprecated - */ -export type LegacyVars = Record; - -type LegacyCoreSetup = InternalCoreSetup & { - plugins: PluginsServiceSetup; - rendering: InternalRenderingServiceSetup; -}; -type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart }; - -/** - * New platform representation of the legacy configuration (KibanaConfig) - * - * @internal - * @deprecated - */ -export interface LegacyConfig { - get(key?: string): T; - has(key: string): boolean; - set(key: string, value: any): void; - set(config: LegacyVars): void; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceSetupDeps { - core: LegacyCoreSetup; - plugins: Record; - uiPlugins: UiPlugins; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceStartDeps { - core: LegacyCoreStart; - plugins: Record; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyServiceSetupConfig { - legacyConfig: LegacyConfig; - settings: LegacyVars; -} diff --git a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap deleted file mode 100644 index fe1407563a635..0000000000000 --- a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`\`schema\` creates correct schema with defaults. 1`] = ` -Object { - "appenders": Map {}, - "loggers": Array [], - "root": Object { - "appenders": Array [ - "default", - ], - "level": "info", - }, -} -`; - -exports[`\`schema\` throws if \`root\` logger does not have "default" appender configured. 1`] = `"[root]: \\"default\\" appender required for migration period till the next major release"`; - -exports[`\`schema\` throws if \`root\` logger does not have appenders configured. 1`] = `"[root.appenders]: array size is [0], but cannot be smaller than [1]"`; - -exports[`fails if loggers use unknown appenders. 1`] = `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`; diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index 83f3c139e371a..e0004ba992c17 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -9,7 +9,35 @@ import { LoggingConfig, config } from './logging_config'; test('`schema` creates correct schema with defaults.', () => { - expect(config.schema.validate({})).toMatchSnapshot(); + expect(config.schema.validate({})).toMatchInlineSnapshot( + { json: expect.any(Boolean) }, // default value depends on TTY + ` + Object { + "appenders": Map {}, + "dest": "stdout", + "events": Object {}, + "filter": Object {}, + "json": Any, + "loggers": Array [], + "quiet": false, + "root": Object { + "appenders": Array [ + "default", + ], + "level": "info", + }, + "rotate": Object { + "enabled": false, + "everyBytes": 10485760, + "keepFiles": 7, + "pollingInterval": 10000, + "usePolling": false, + }, + "silent": false, + "verbose": false, + } + ` + ); }); test('`schema` throws if `root` logger does not have appenders configured.', () => { @@ -19,7 +47,9 @@ test('`schema` throws if `root` logger does not have appenders configured.', () appenders: [], }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[root.appenders]: array size is [0], but cannot be smaller than [1]"` + ); }); test('`schema` throws if `root` logger does not have "default" appender configured.', () => { @@ -29,7 +59,9 @@ test('`schema` throws if `root` logger does not have "default" appender configur appenders: ['console'], }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot( + `"[root]: \\"default\\" appender required for migration period till the next major release"` + ); }); test('`getParentLoggerContext()` returns correct parent context name.', () => { @@ -157,7 +189,9 @@ test('fails if loggers use unknown appenders.', () => { ], }); - expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingSnapshot(); + expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingInlineSnapshot( + `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."` + ); }); describe('extend', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 24496289fb4c8..f5b75d7bb739c 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -7,6 +7,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; import { AppenderConfigType, Appenders } from './appenders/appenders'; // We need this helper for the types to be correct @@ -59,7 +60,7 @@ export const loggerSchema = schema.object({ export type LoggerConfigType = TypeOf; export const config = { path: 'logging', - schema: schema.object({ + schema: legacyLoggingConfigSchema.extends({ appenders: schema.mapOf(schema.string(), Appenders.configSchema, { defaultValue: new Map(), }), @@ -85,7 +86,7 @@ export const config = { }), }; -export type LoggingConfigType = Omit, 'appenders'> & { +export type LoggingConfigType = Pick, 'loggers' | 'root'> & { appenders: Map; }; @@ -105,6 +106,7 @@ export const loggerContextConfigSchema = schema.object({ /** @public */ export type LoggerContextConfigType = TypeOf; + /** @public */ export interface LoggerContextConfigInput { // config-schema knows how to handle either Maps or Records diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index 8a6fe71bc6222..b67be384732cb 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -16,6 +16,7 @@ jest.mock('fs', () => ({ const dynamicProps = { process: { pid: expect.any(Number) } }; jest.mock('@kbn/legacy-logging', () => ({ + ...(jest.requireActual('@kbn/legacy-logging') as any), setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), })); diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts index 3e358edf3a01e..0631bb2b35801 100644 --- a/src/core/server/metrics/index.ts +++ b/src/core/server/metrics/index.ts @@ -16,3 +16,4 @@ export type { export type { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; export { MetricsService } from './metrics_service'; export { opsConfig } from './ops_config'; +export type { OpsConfigType } from './ops_config'; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 19056ae1b9bc7..6f1b9dc5bf820 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -29,6 +29,7 @@ import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; import { i18nServiceMock } from './i18n/i18n_service.mock'; +import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -49,6 +50,9 @@ export { contextServiceMock } from './context/context_service.mock'; export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; export { i18nServiceMock } from './i18n/i18n_service.mock'; +export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; + +type MockedPluginInitializerConfig = jest.Mocked['config']>; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -68,7 +72,7 @@ export function pluginInitializerContextConfigMock(config: T) { }, }; - const mock: jest.Mocked['config']> = { + const mock: MockedPluginInitializerConfig = { legacy: { globalConfig$: of(globalConfig), get: () => globalConfig, @@ -80,8 +84,12 @@ export function pluginInitializerContextConfigMock(config: T) { return mock; } +type PluginInitializerContextMock = Omit, 'config'> & { + config: MockedPluginInitializerConfig; +}; + function pluginInitializerContextMock(config: T = {} as T) { - const mock: PluginInitializerContext = { + const mock: PluginInitializerContextMock = { opaqueId: Symbol(), logger: loggingSystemMock.create(), env: { @@ -137,6 +145,7 @@ function createCoreSetupMock({ uiSettings: uiSettingsMock, logging: loggingServiceMock.createSetupContract(), metrics: metricsServiceMock.createSetupContract(), + deprecations: deprecationsServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -174,6 +183,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), + deprecations: deprecationsServiceMock.createInternalSetupContract(), }; return setupDeps; } diff --git a/src/core/server/plugins/legacy_config.test.ts b/src/core/server/plugins/legacy_config.test.ts index 5687c2dd551d2..0ea26f2e0333e 100644 --- a/src/core/server/plugins/legacy_config.test.ts +++ b/src/core/server/plugins/legacy_config.test.ts @@ -13,7 +13,7 @@ import { getGlobalConfig, getGlobalConfig$ } from './legacy_config'; import { REPO_ROOT } from '@kbn/utils'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { duration } from 'moment'; -import { fromRoot } from '../utils'; +import { fromRoot } from '@kbn/utils'; import { ByteSizeValue } from '@kbn/config-schema'; import { Server } from '../server'; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index b10bc47cb825b..e37d985d42321 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -9,6 +9,7 @@ import { duration } from 'moment'; import { first } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { fromRoot } from '@kbn/utils'; import { createPluginInitializerContext, InstanceInfo } from './plugin_context'; import { CoreContext } from '../core_context'; import { Env } from '../config'; @@ -16,7 +17,6 @@ import { loggingSystemMock } from '../logging/logging_system.mock'; import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; import { PluginManifest } from './types'; import { Server } from '../server'; -import { fromRoot } from '../utils'; import { schema, ByteSizeValue } from '@kbn/config-schema'; import { ConfigService } from '@kbn/config'; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 74451f38b893e..c466eb2b9ee09 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -165,6 +165,7 @@ export function createPluginSetupContext( register: deps.uiSettings.register, }, getStartServices: () => plugin.startDependencies, + deprecations: deps.deprecations.getRegistry(plugin.name), }; } diff --git a/src/core/server/plugins/plugins_config.ts b/src/core/server/plugins/plugins_config.ts index d565513ebb35b..45d80445f376e 100644 --- a/src/core/server/plugins/plugins_config.ts +++ b/src/core/server/plugins/plugins_config.ts @@ -7,20 +7,24 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { ServiceConfigDescriptor } from '../internal_types'; import { Env } from '../config'; -export type PluginsConfigType = TypeOf; +const configSchema = schema.object({ + initialize: schema.boolean({ defaultValue: true }), -export const config = { + /** + * Defines an array of directories where another plugin should be loaded from. + */ + paths: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export type PluginsConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { path: 'plugins', - schema: schema.object({ - initialize: schema.boolean({ defaultValue: true }), - - /** - * Defines an array of directories where another plugin should be loaded from. - */ - paths: schema.arrayOf(schema.string(), { defaultValue: [] }), - }), + schema: configSchema, + deprecations: ({ unusedFromRoot }) => [unusedFromRoot('plugins.scanDirs')], }; /** @internal */ diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 2d54648d22950..6bf7a1fadb4d3 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -562,12 +562,12 @@ describe('PluginsService', () => { plugin$: from([ createPlugin('plugin-1', { path: 'path-1', - version: 'some-version', + version: 'version-1', configPath: 'plugin1', }), createPlugin('plugin-2', { path: 'path-2', - version: 'some-version', + version: 'version-2', configPath: 'plugin2', }), ]), @@ -577,7 +577,7 @@ describe('PluginsService', () => { }); describe('uiPlugins.internal', () => { - it('includes disabled plugins', async () => { + it('contains internal properties for plugins', async () => { config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); expect(uiPlugins.internal).toMatchInlineSnapshot(` @@ -586,15 +586,23 @@ describe('PluginsService', () => { "publicAssetsDir": /path-1/public/assets, "publicTargetDir": /path-1/target/public, "requiredBundles": Array [], + "version": "version-1", }, "plugin-2" => Object { "publicAssetsDir": /path-2/public/assets, "publicTargetDir": /path-2/target/public, "requiredBundles": Array [], + "version": "version-2", }, } `); }); + + it('includes disabled plugins', async () => { + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); + const { uiPlugins } = await pluginsService.discover({ environment: environmentSetup }); + expect([...uiPlugins.internal.keys()].sort()).toEqual(['plugin-1', 'plugin-2']); + }); }); describe('plugin initialization', () => { diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 8b33e2cf4cc6b..09be40ecaf2a2 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -222,6 +222,7 @@ export class PluginsService implements CoreService(); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a6086bd6f17e8..3a01049c5e1fe 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -224,12 +224,15 @@ export interface DiscoveredPlugin { */ export interface InternalPluginInfo { /** - * Bundles that must be loaded for this plugoin + * Version of the plugin + */ + readonly version: string; + /** + * Bundles that must be loaded for this plugin */ readonly requiredBundles: readonly string[]; /** - * Path to the target/public directory of the plugin which should be - * served + * Path to the target/public directory of the plugin which should be served */ readonly publicTargetDir: string; /** @@ -250,7 +253,9 @@ export interface Plugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup; + start(core: CoreStart, plugins: TPluginsStart): TStart; + stop?(): void; } @@ -267,7 +272,9 @@ export interface AsyncPlugin< TPluginsStart extends object = object > { setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; + stop?(): void; } diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts index ea3843884df31..0abd8fd5a0057 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { UiPlugins } from '../../plugins'; +import { InternalPluginInfo, UiPlugins } from '../../plugins'; import { getPluginsBundlePaths } from './get_plugin_bundle_paths'; const createUiPlugins = (pluginDeps: Record) => { @@ -16,12 +16,13 @@ const createUiPlugins = (pluginDeps: Record) => { browserConfigs: new Map(), }; - Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + const addPlugin = (pluginId: string, deps: string[]) => { uiPlugins.internal.set(pluginId, { requiredBundles: deps, + version: '8.0.0', publicTargetDir: '', publicAssetsDir: '', - } as any); + } as InternalPluginInfo); uiPlugins.public.set(pluginId, { id: pluginId, configPath: 'config-path', @@ -29,6 +30,12 @@ const createUiPlugins = (pluginDeps: Record) => { requiredPlugins: [], requiredBundles: deps, }); + + deps.forEach((dep) => addPlugin(dep, [])); + }; + + Object.entries(pluginDeps).forEach(([pluginId, deps]) => { + addPlugin(pluginId, deps); }); return uiPlugins; @@ -56,13 +63,13 @@ describe('getPluginsBundlePaths', () => { }); expect(pluginBundlePaths.get('a')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/a/a.plugin.js', - publicPath: '/regular-bundle-path/plugin/a/', + bundlePath: '/regular-bundle-path/plugin/a/8.0.0/a.plugin.js', + publicPath: '/regular-bundle-path/plugin/a/8.0.0/', }); expect(pluginBundlePaths.get('b')).toEqual({ - bundlePath: '/regular-bundle-path/plugin/b/b.plugin.js', - publicPath: '/regular-bundle-path/plugin/b/', + bundlePath: '/regular-bundle-path/plugin/b/8.0.0/b.plugin.js', + publicPath: '/regular-bundle-path/plugin/b/8.0.0/', }); }); }); diff --git a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts index c8291b2720a92..86ffdcf835f7b 100644 --- a/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts +++ b/src/core/server/rendering/bootstrap/get_plugin_bundle_paths.ts @@ -25,9 +25,15 @@ export const getPluginsBundlePaths = ({ while (pluginsToProcess.length > 0) { const pluginId = pluginsToProcess.pop() as string; + const plugin = uiPlugins.internal.get(pluginId); + if (!plugin) { + continue; + } + const { version } = plugin; + pluginBundlePaths.set(pluginId, { - publicPath: `${regularBundlePath}/plugin/${pluginId}/`, - bundlePath: `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`, + publicPath: `${regularBundlePath}/plugin/${pluginId}/${version}/`, + bundlePath: `${regularBundlePath}/plugin/${pluginId}/${version}/${pluginId}.plugin.js`, }); const pluginBundleIds = uiPlugins.internal.get(pluginId)?.requiredBundles ?? []; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index 61de31e825d33..530203e659086 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -35,13 +35,14 @@ const createMigrator = ( ) => { const mockMigrator: jest.Mocked = { kibanaVersion: '8.0.0-testing', - savedObjectsConfig: { + soMigrationsConfig: { batchSize: 100, scrollDuration: '15m', pollInterval: 1500, skip: false, - // TODO migrationsV2: remove/deprecate once we release migrations v2 + // TODO migrationsV2: remove/deprecate once we remove migrations v1 enableV2: false, + retryAttempts: 10, }, runMigrations: jest.fn(), getActiveMappings: jest.fn(), diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 7ead37699980a..40d18c3b5063a 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -414,12 +414,13 @@ const mockOptions = ({ enableV2 }: { enableV2: boolean } = { enableV2: false }) enabled: true, index: '.my-index', } as KibanaMigratorOptions['kibanaConfig'], - savedObjectsConfig: { + soMigrationsConfig: { batchSize: 20, pollInterval: 20000, scrollDuration: '10m', skip: false, enableV2, + retryAttempts: 20, }, client: elasticsearchClientMock.createElasticsearchClient(), }; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index e5c64914e4c96..29852f8ac6445 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -41,7 +41,7 @@ import { MigrationLogger } from '../core/migration_logger'; export interface KibanaMigratorOptions { client: ElasticsearchClient; typeRegistry: ISavedObjectTypeRegistry; - savedObjectsConfig: SavedObjectsMigrationConfigType; + soMigrationsConfig: SavedObjectsMigrationConfigType; kibanaConfig: KibanaConfigType; kibanaVersion: string; logger: Logger; @@ -72,10 +72,10 @@ export class KibanaMigrator { }); private readonly activeMappings: IndexMapping; private migrationsRetryDelay?: number; - // TODO migrationsV2: make private once we release migrations v2 - public kibanaVersion: string; - // TODO migrationsV2: make private once we release migrations v2 - public readonly savedObjectsConfig: SavedObjectsMigrationConfigType; + // TODO migrationsV2: make private once we remove migrations v1 + public readonly kibanaVersion: string; + // TODO migrationsV2: make private once we remove migrations v1 + public readonly soMigrationsConfig: SavedObjectsMigrationConfigType; /** * Creates an instance of KibanaMigrator. @@ -84,14 +84,14 @@ export class KibanaMigrator { client, typeRegistry, kibanaConfig, - savedObjectsConfig, + soMigrationsConfig, kibanaVersion, logger, migrationsRetryDelay, }: KibanaMigratorOptions) { this.client = client; this.kibanaConfig = kibanaConfig; - this.savedObjectsConfig = savedObjectsConfig; + this.soMigrationsConfig = soMigrationsConfig; this.typeRegistry = typeRegistry; this.serializer = new SavedObjectsSerializer(this.typeRegistry); this.mappingProperties = mergeTypes(this.typeRegistry.getAllTypes()); @@ -175,7 +175,7 @@ export class KibanaMigrator { const migrators = Object.keys(indexMap).map((index) => { // TODO migrationsV2: remove old migrations algorithm - if (this.savedObjectsConfig.enableV2) { + if (this.soMigrationsConfig.enableV2) { return { migrate: (): Promise => { return runResilientMigrator({ @@ -193,20 +193,21 @@ export class KibanaMigrator { ), migrationVersionPerType: this.documentMigrator.migrationVersion, indexPrefix: index, + migrationsConfig: this.soMigrationsConfig, }); }, }; } else { return new IndexMigrator({ - batchSize: this.savedObjectsConfig.batchSize, + batchSize: this.soMigrationsConfig.batchSize, client: createMigrationEsClient(this.client, this.log, this.migrationsRetryDelay), documentMigrator: this.documentMigrator, index, kibanaVersion: this.kibanaVersion, log: this.log, mappingProperties: indexMap[index].typeMappings, - pollInterval: this.savedObjectsConfig.pollInterval, - scrollDuration: this.savedObjectsConfig.scrollDuration, + pollInterval: this.soMigrationsConfig.pollInterval, + scrollDuration: this.soMigrationsConfig.scrollDuration, serializer: this.serializer, // Only necessary for the migrator of the kibana index. obsoleteIndexTemplatePattern: diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 22dfb03815052..52fa99b724873 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -9,7 +9,7 @@ import * as Either from 'fp-ts/lib/Either'; import * as TaskEither from 'fp-ts/lib/TaskEither'; import * as Option from 'fp-ts/lib/Option'; -import { ElasticsearchClientError } from '@elastic/elasticsearch/lib/errors'; +import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; import { pipe } from 'fp-ts/lib/pipeable'; import { errors as EsErrors } from '@elastic/elasticsearch'; import { flow } from 'fp-ts/lib/function'; @@ -23,12 +23,6 @@ import { } from './catch_retryable_es_client_errors'; export type { RetryableEsClientError }; -export const isRetryableEsClientResponse = ( - res: Either.Either -): res is Either.Left => { - return Either.isLeft(res) && res.left.type === 'retryable_es_client_error'; -}; - /** * Batch size for updateByQuery, reindex & search operations. Smaller batches * reduce the memory pressure on Elasticsearch and Kibana so are less likely @@ -45,6 +39,27 @@ const INDEX_NUMBER_OF_SHARDS = 1; /** Wait for all shards to be active before starting an operation */ const WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE = 'all'; +// Map of left response 'type' string -> response interface +export interface ActionErrorTypeMap { + wait_for_task_completion_timeout: WaitForTaskCompletionTimeout; + retryable_es_client_error: RetryableEsClientError; + index_not_found_exception: IndexNotFound; + target_index_had_write_block: TargetIndexHadWriteBlock; + incompatible_mapping_exception: IncompatibleMappingException; + alias_not_found_exception: AliasNotFound; + remove_index_not_a_concrete_index: RemoveIndexNotAConcreteIndex; +} + +/** + * Type guard for narrowing the type of a left + */ +export function isLeftTypeof( + res: any, + typeString: T +): res is ActionErrorTypeMap[T] { + return res.type === typeString; +} + export type FetchIndexResponse = Record< string, { aliases: Record; mappings: IndexMapping; settings: unknown } @@ -74,6 +89,10 @@ export const fetchIndices = ( .catch(catchRetryableEsClientErrors); }; +export interface IndexNotFound { + type: 'index_not_found_exception'; + index: string; +} /** * Sets a write block in place for the given index. If the response includes * `acknowledged: true` all in-progress writes have drained and no further @@ -87,7 +106,7 @@ export const setWriteBlock = ( client: ElasticsearchClient, index: string ): TaskEither.TaskEither< - { type: 'index_not_found_exception' } | RetryableEsClientError, + IndexNotFound | RetryableEsClientError, 'set_write_block_succeeded' > => () => { return client.indices @@ -112,7 +131,7 @@ export const setWriteBlock = ( .catch((e: ElasticsearchClientError) => { if (e instanceof EsErrors.ResponseError) { if (e.message === 'index_not_found_exception') { - return Either.left({ type: 'index_not_found_exception' as const }); + return Either.left({ type: 'index_not_found_exception' as const, index }); } } throw e; @@ -170,10 +189,11 @@ export const removeWriteBlock = ( */ const waitForIndexStatusYellow = ( client: ElasticsearchClient, - index: string + index: string, + timeout: string ): TaskEither.TaskEither => () => { return client.cluster - .health({ index, wait_for_status: 'yellow', timeout: '30s' }) + .health({ index, wait_for_status: 'yellow', timeout }) .then(() => { return Either.right({}); }) @@ -189,19 +209,18 @@ export type CloneIndexResponse = AcknowledgeResponse; * This method adds some additional logic to the ES clone index API: * - it is idempotent, if it gets called multiple times subsequent calls will * wait for the first clone operation to complete (up to 60s) - * - the first call will wait up to 90s for the cluster state and all shards + * - the first call will wait up to 120s for the cluster state and all shards * to be updated. */ export const cloneIndex = ( client: ElasticsearchClient, source: string, - target: string -): TaskEither.TaskEither< - RetryableEsClientError | { type: 'index_not_found_exception'; index: string }, - CloneIndexResponse -> => { + target: string, + /** only used for testing */ + timeout = DEFAULT_TIMEOUT +): TaskEither.TaskEither => { const cloneTask: TaskEither.TaskEither< - RetryableEsClientError | { type: 'index_not_found_exception'; index: string }, + RetryableEsClientError | IndexNotFound, AcknowledgeResponse > = () => { return client.indices @@ -227,7 +246,7 @@ export const cloneIndex = ( }, }, }, - timeout: DEFAULT_TIMEOUT, + timeout, }, { maxRetries: 0 /** handle retry ourselves for now */ } ) @@ -277,7 +296,7 @@ export const cloneIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusYellow(client, target), + waitForIndexStatusYellow(client, target, timeout), TaskEither.map((value) => { /** When the index status is 'green' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; @@ -295,6 +314,38 @@ interface WaitForTaskResponse { description?: string; } +/** + * After waiting for the specificed timeout, the task has not yet completed. + * + * When querying the tasks API we use `wait_for_completion=true` to block the + * request until the task completes. If after the `timeout`, the task still has + * not completed we return this error. This does not mean that the task itelf + * has reached a timeout, Elasticsearch will continue to run the task. + */ +export interface WaitForTaskCompletionTimeout { + /** After waiting for the specificed timeout, the task has not yet completed. */ + readonly type: 'wait_for_task_completion_timeout'; + readonly message: string; + readonly error?: Error; +} + +const catchWaitForTaskCompletionTimeout = ( + e: ResponseError +): Either.Either => { + if ( + e.body?.error?.type === 'timeout_exception' || + e.body?.error?.type === 'receive_timeout_transport_exception' + ) { + return Either.left({ + type: 'wait_for_task_completion_timeout' as const, + message: `[${e.body.error.type}] ${e.body.error.reason}`, + error: e, + }); + } else { + throw e; + } +}; + /** * Blocks for up to 60s or until a task completes. * @@ -304,7 +355,10 @@ const waitForTask = ( client: ElasticsearchClient, taskId: string, timeout: string -): TaskEither.TaskEither => () => { +): TaskEither.TaskEither< + RetryableEsClientError | WaitForTaskCompletionTimeout, + WaitForTaskResponse +> => () => { return client.tasks .get({ task_id: taskId, @@ -322,6 +376,7 @@ const waitForTask = ( description: body.task.description, }); }) + .catch(catchWaitForTaskCompletionTimeout) .catch(catchRetryableEsClientErrors); }; @@ -424,7 +479,15 @@ export const reindex = ( }; interface WaitForReindexTaskFailure { - cause: { type: string; reason: string }; + readonly cause: { type: string; reason: string }; +} + +export interface TargetIndexHadWriteBlock { + type: 'target_index_had_write_block'; +} + +export interface IncompatibleMappingException { + type: 'incompatible_mapping_exception'; } export const waitForReindexTask = flow( @@ -433,10 +496,11 @@ export const waitForReindexTask = flow( ( res ): TaskEither.TaskEither< - | { type: 'index_not_found_exception'; index: string } - | { type: 'target_index_had_write_block' } - | { type: 'incompatible_mapping_exception' } - | RetryableEsClientError, + | IndexNotFound + | TargetIndexHadWriteBlock + | IncompatibleMappingException + | RetryableEsClientError + | WaitForTaskCompletionTimeout, 'reindex_succeeded' > => { const failureIsAWriteBlock = ({ cause: { type, reason } }: WaitForReindexTaskFailure) => @@ -507,7 +571,12 @@ export const verifyReindex = ( export const waitForPickupUpdatedMappingsTask = flow( waitForTask, TaskEither.chain( - (res): TaskEither.TaskEither => { + ( + res + ): TaskEither.TaskEither< + RetryableEsClientError | WaitForTaskCompletionTimeout, + 'pickup_updated_mappings_succeeded' + > => { // We don't catch or type failures/errors because they should never // occur in our migration algorithm and we don't have any business logic // for dealing with it. If something happens we'll just crash and try @@ -529,6 +598,14 @@ export const waitForPickupUpdatedMappingsTask = flow( ) ); +export interface AliasNotFound { + type: 'alias_not_found_exception'; +} + +export interface RemoveIndexNotAConcreteIndex { + type: 'remove_index_not_a_concrete_index'; +} + export type AliasAction = | { remove_index: { index: string } } | { remove: { index: string; alias: string; must_exist: boolean } } @@ -541,10 +618,7 @@ export const updateAliases = ( client: ElasticsearchClient, aliasActions: AliasAction[] ): TaskEither.TaskEither< - | { type: 'index_not_found_exception'; index: string } - | { type: 'alias_not_found_exception' } - | { type: 'remove_index_not_a_concrete_index' } - | RetryableEsClientError, + IndexNotFound | AliasNotFound | RemoveIndexNotAConcreteIndex | RetryableEsClientError, 'update_aliases_succeeded' > => () => { return client.indices @@ -698,11 +772,11 @@ export const createIndex = ( // If the cluster state was updated and all shards ackd we're done return TaskEither.right('create_index_succeeded'); } else { - // Otherwise, wait until the target index has a 'green' status. + // Otherwise, wait until the target index has a 'yellow' status. return pipe( - waitForIndexStatusYellow(client, indexName), + waitForIndexStatusYellow(client, indexName, DEFAULT_TIMEOUT), TaskEither.map(() => { - /** When the index status is 'green' we know that all shards were started */ + /** When the index status is 'yellow' we know that all shards were started */ return 'create_index_succeeded'; }) ); diff --git a/src/core/server/saved_objects/migrationsv2/index.ts b/src/core/server/saved_objects/migrationsv2/index.ts index 0297aefdc7abd..6e65a2e700fd3 100644 --- a/src/core/server/saved_objects/migrationsv2/index.ts +++ b/src/core/server/saved_objects/migrationsv2/index.ts @@ -14,6 +14,7 @@ import { MigrationResult } from '../migrations/core'; import { next, TransformRawDocs } from './next'; import { createInitialState, model } from './model'; import { migrationStateActionMachine } from './migrations_state_action_machine'; +import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; /** * Migrates the provided indexPrefix index using a resilient algorithm that is @@ -29,6 +30,7 @@ export async function runResilientMigrator({ transformRawDocs, migrationVersionPerType, indexPrefix, + migrationsConfig, }: { client: ElasticsearchClient; kibanaVersion: string; @@ -38,6 +40,7 @@ export async function runResilientMigrator({ transformRawDocs: TransformRawDocs; migrationVersionPerType: SavedObjectsMigrationVersion; indexPrefix: string; + migrationsConfig: SavedObjectsMigrationConfigType; }): Promise { const initialState = createInitialState({ kibanaVersion, @@ -45,6 +48,7 @@ export async function runResilientMigrator({ preMigrationScript, migrationVersionPerType, indexPrefix, + migrationsConfig, }); return migrationStateActionMachine({ initialState, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index 2c052a87d028b..1824efa0ed8d4 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -33,6 +33,7 @@ import { } from '../actions'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t: number) => jest.setTimeout(t), @@ -162,6 +163,7 @@ describe('migration actions', () => { Object { "_tag": "Left", "left": Object { + "index": "no_such_index", "type": "index_not_found_exception", }, } @@ -291,6 +293,45 @@ describe('migration actions', () => { } `); }); + it('resolves left with a retryable_es_client_error if clone target already exists but takes longer than the specified timeout before turning yellow', async () => { + // Create a red index + await client.indices + .create({ + index: 'clone_red_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate 1 replica so that this index stays yellow + number_of_replicas: '1', + // Disable all shard allocation so that the index status is red + 'index.routing.allocation.enable': 'none', + }, + }, + }) + .catch((e) => {}); + + // Call clone even though the index already exists + const cloneIndexPromise = cloneIndex( + client, + 'existing_index_with_write_block', + 'clone_red_index', + '0s' + )(); + + await cloneIndexPromise.then((res) => { + expect(res).toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "error": [ResponseError: Response Error], + "message": "Response Error", + "type": "retryable_es_client_error", + }, + } + `); + }); + }); }); // Reindex doesn't return any errors on it's own, so we have to test @@ -587,6 +628,28 @@ describe('migration actions', () => { } `); }); + it('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { + const res = (await reindex( + client, + 'existing_index_with_docs', + 'reindex_target', + Option.none, + false + )()) as Either.Right; + + const task = waitForReindexTask(client, res.right.taskId, '0s'); + + await expect(task()).resolves.toMatchObject({ + _tag: 'Left', + left: { + error: expect.any(ResponseError), + message: expect.stringMatching( + /\[timeout_exception\] Timed out waiting for completion of \[org.elasticsearch.index.reindex.BulkByScrollTask/ + ), + type: 'wait_for_task_completion_timeout', + }, + }); + }); }); describe('verifyReindex', () => { @@ -702,6 +765,25 @@ describe('migration actions', () => { {"type":"index_not_found_exception","reason":"no such index [no_such_index]","resource.type":"index_or_alias","resource.id":"no_such_index","index_uuid":"_na_","index":"no_such_index"}] `); }); + it('resolves left wait_for_task_completion_timeout when the task does not complete within the timeout', async () => { + const res = (await pickupUpdatedMappings( + client, + 'existing_index_with_docs' + )()) as Either.Right; + + const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '0s'); + + await expect(task()).resolves.toMatchObject({ + _tag: 'Left', + left: { + error: expect.any(ResponseError), + message: expect.stringMatching( + /\[timeout_exception\] Timed out waiting for completion of \[org.elasticsearch.index.reindex.BulkByScrollTask/ + ), + type: 'wait_for_task_completion_timeout', + }, + }); + }); it('resolves right when successful', async () => { const res = (await pickupUpdatedMappings( client, diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index f7b9c4c368fa0..99c06c0a3586b 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -27,6 +27,14 @@ describe('migrationsStateActionMachine', () => { targetMappings: { properties: {} }, migrationVersionPerType: {}, indexPrefix: '.my-so-index', + migrationsConfig: { + batchSize: 1000, + pollInterval: 0, + scrollDuration: '0s', + skip: false, + enableV2: true, + retryAttempts: 5, + }, }); const next = jest.fn((s: State) => { @@ -221,6 +229,7 @@ describe('migrationsStateActionMachine', () => { "_tag": "None", }, "reason": "the fatal reason", + "retryAttempts": 5, "retryCount": 0, "retryDelay": 0, "targetIndexMappings": Object { @@ -280,6 +289,7 @@ describe('migrationsStateActionMachine', () => { "_tag": "None", }, "reason": "the fatal reason", + "retryAttempts": 5, "retryCount": 0, "retryDelay": 0, "targetIndexMappings": Object { @@ -424,6 +434,7 @@ describe('migrationsStateActionMachine', () => { "_tag": "None", }, "reason": "the fatal reason", + "retryAttempts": 5, "retryCount": 0, "retryDelay": 0, "targetIndexMappings": Object { @@ -478,6 +489,7 @@ describe('migrationsStateActionMachine', () => { "_tag": "None", }, "reason": "the fatal reason", + "retryAttempts": 5, "retryCount": 0, "retryDelay": 0, "targetIndexMappings": Object { diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 5531f847f8bb4..2813f01093e95 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -35,6 +35,7 @@ import { SavedObjectsRawDoc } from '..'; import { AliasAction, RetryableEsClientError } from './actions'; import { createInitialState, model } from './model'; import { ResponseType } from './next'; +import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; describe('migrations v2 model', () => { const baseState: BaseState = { @@ -44,6 +45,7 @@ describe('migrations v2 model', () => { logs: [], retryCount: 0, retryDelay: 0, + retryAttempts: 15, indexPrefix: '.kibana', outdatedDocumentsQuery: {}, targetIndexMappings: { @@ -160,15 +162,15 @@ describe('migrations v2 model', () => { expect(newState.retryDelay).toEqual(0); }); - test('terminates to FATAL after 10 retries', () => { + test('terminates to FATAL after retryAttempts retries', () => { const newState = model( - { ...state, ...{ retryCount: 10, retryDelay: 64000 } }, + { ...state, ...{ retryCount: 15, retryDelay: 64000 } }, Either.left(retryableError) ) as FatalState; expect(newState.controlState).toEqual('FATAL'); expect(newState.reason).toMatchInlineSnapshot( - `"Unable to complete the INIT step after 10 attempts, terminating."` + `"Unable to complete the INIT step after 15 attempts, terminating."` ); }); }); @@ -610,6 +612,7 @@ describe('migrations v2 model', () => { test('LEGACY_SET_WRITE_BLOCK -> LEGACY_CREATE_REINDEX_TARGET if action fails with index_not_found_exception', () => { const res: ResponseType<'LEGACY_SET_WRITE_BLOCK'> = Either.left({ type: 'index_not_found_exception', + index: 'legacy_index_name', }); const newState = model(legacySetWriteBlockState, res); expect(newState.controlState).toEqual('LEGACY_CREATE_REINDEX_TARGET'); @@ -707,6 +710,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('LEGACY_REINDEX_WAIT_FOR_TASK -> LEGACY_REINDEX_WAIT_FOR_TASK if action fails with wait_for_task_completion_timeout', () => { + const res: ResponseType<'LEGACY_REINDEX_WAIT_FOR_TASK'> = Either.left({ + message: '[timeout_exception] Timeout waiting for ...', + type: 'wait_for_task_completion_timeout', + }); + const newState = model(legacyReindexWaitForTaskState, res); + expect(newState.controlState).toEqual('LEGACY_REINDEX_WAIT_FOR_TASK'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); }); describe('LEGACY_DELETE', () => { const legacyDeleteState: LegacyDeleteState = { @@ -846,6 +859,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK -> REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK when response is left wait_for_task_completion_timeout', () => { + const res: ResponseType<'REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'> = Either.left({ + message: '[timeout_exception] Timeout waiting for ...', + type: 'wait_for_task_completion_timeout', + }); + const newState = model(state, res); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_WAIT_FOR_TASK'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); }); describe('SET_TEMP_WRITE_BLOCK', () => { const state: SetTempWriteBlock = { @@ -1025,6 +1048,19 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK -> UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK when response is left wait_for_task_completion_timeout', () => { + const res: ResponseType<'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'> = Either.left({ + message: '[timeout_exception] Timeout waiting for ...', + type: 'wait_for_task_completion_timeout', + }); + const newState = model( + updateTargetMappingsWaitForTaskState, + res + ) as UpdateTargetMappingsWaitForTaskState; + expect(newState.controlState).toEqual('UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); }); describe('CREATE_NEW_TARGET', () => { const aliasActions = Option.some([Symbol('alias action')] as unknown) as Option.Some< @@ -1144,6 +1180,9 @@ describe('migrations v2 model', () => { }); }); describe('createInitialState', () => { + const migrationsConfig = ({ + retryAttempts: 15, + } as unknown) as SavedObjectsMigrationConfigType; it('creates the initial state for the model based on the passed in paramaters', () => { expect( createInitialState({ @@ -1154,6 +1193,7 @@ describe('migrations v2 model', () => { }, migrationVersionPerType: {}, indexPrefix: '.kibana_task_manager', + migrationsConfig, }) ).toMatchInlineSnapshot(` Object { @@ -1171,6 +1211,7 @@ describe('migrations v2 model', () => { "preMigrationScript": Object { "_tag": "None", }, + "retryAttempts": 15, "retryCount": 0, "retryDelay": 0, "targetIndexMappings": Object { @@ -1214,6 +1255,7 @@ describe('migrations v2 model', () => { preMigrationScript, migrationVersionPerType: {}, indexPrefix: '.kibana_task_manager', + migrationsConfig, }); expect(Option.isSome(initialState.preMigrationScript)).toEqual(true); @@ -1233,6 +1275,7 @@ describe('migrations v2 model', () => { preMigrationScript: undefined, migrationVersionPerType: {}, indexPrefix: '.kibana_task_manager', + migrationsConfig, }).preMigrationScript ) ).toEqual(true); @@ -1248,6 +1291,7 @@ describe('migrations v2 model', () => { preMigrationScript: "ctx._id = ctx._source.type + ':' + ctx._id", migrationVersionPerType: { my_dashboard: '7.10.1', my_viz: '8.0.0' }, indexPrefix: '.kibana_task_manager', + migrationsConfig, }).outdatedDocumentsQuery ).toMatchInlineSnapshot(` Object { diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 2e92f34429ea9..5bdba98026792 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -10,33 +10,13 @@ import { gt, valid } from 'semver'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; import { cloneDeep } from 'lodash'; -import { AliasAction, FetchIndexResponse, RetryableEsClientError } from './actions'; +import { AliasAction, FetchIndexResponse, isLeftTypeof, RetryableEsClientError } from './actions'; import { AllActionStates, InitState, State } from './types'; import { IndexMapping } from '../mappings'; import { ResponseType } from './next'; import { SavedObjectsMigrationVersion } from '../types'; import { disableUnknownTypeMappingFields } from '../migrations/core/migration_context'; - -/** - * How many times to retry a failing step. - * - * Waiting for a task to complete will cause a failing step every time the - * wait_for_task action times out e.g. the following sequence has 3 retry - * attempts: - * LEGACY_REINDEX_WAIT_FOR_TASK (60s timeout) -> - * LEGACY_REINDEX_WAIT_FOR_TASK (2s delay, 60s timeout) -> - * LEGACY_REINDEX_WAIT_FOR_TASK (4s delay, 60s timeout) -> - * LEGACY_REINDEX_WAIT_FOR_TASK (success) -> ... - * - * This places an upper limit to how long we will wait for a task to complete. - * The duration of a step is the time it takes for the action to complete plus - * the exponential retry delay: - * max_task_runtime = 2+4+8+16+32+64*(MAX_RETRY_ATTEMPTS-5) + ACTION_DURATION*MAX_RETRY_ATTEMPTS - * - * For MAX_RETRY_ATTEMPTS=10, ACTION_DURATION=60 - * max_task_runtime = 16.46 minutes - */ -const MAX_RETRY_ATTEMPTS = 10; +import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; /** * A helper function/type for ensuring that all control state's are handled. @@ -115,12 +95,17 @@ function getAliases(indices: FetchIndexResponse) { }, {} as Record); } -const delayRetryState = (state: S, left: RetryableEsClientError): S => { - if (state.retryCount === MAX_RETRY_ATTEMPTS) { +const delayRetryState = ( + state: S, + errorMessage: string, + /** How many times to retry a step that fails */ + maxRetryAttempts: number +): S => { + if (state.retryCount >= maxRetryAttempts) { return { ...state, controlState: 'FATAL', - reason: `Unable to complete the ${state.controlState} step after ${MAX_RETRY_ATTEMPTS} attempts, terminating.`, + reason: `Unable to complete the ${state.controlState} step after ${maxRetryAttempts} attempts, terminating.`, }; } else { const retryCount = state.retryCount + 1; @@ -134,9 +119,7 @@ const delayRetryState = (state: S, left: RetryableEsClientError ...state.logs, { level: 'error', - message: `Action failed with '${ - left.message - }'. Retrying attempt ${retryCount} out of ${MAX_RETRY_ATTEMPTS} in ${ + message: `Action failed with '${errorMessage}'. Retrying attempt ${retryCount} in ${ retryDelay / 1000 } seconds.`, }, @@ -175,9 +158,12 @@ export const model = (currentState: State, resW: ResponseType): // Handle retryable_es_client_errors. Other left values need to be handled // by the control state specific code below. - if (Either.isLeft(resW) && resW.left.type === 'retryable_es_client_error') { + if ( + Either.isLeft(resW) && + isLeftTypeof(resW.left, 'retryable_es_client_error') + ) { // Retry the same step after an exponentially increasing delay. - return delayRetryState(stateP, resW.left); + return delayRetryState(stateP, resW.left.message, stateP.retryAttempts); } else { // If the action didn't fail with a retryable_es_client_error, reset the // retry counter and retryDelay state @@ -333,7 +319,7 @@ export const model = (currentState: State, resW: ResponseType): // If the write block failed because the index doesn't exist, it means // another instance already completed the legacy pre-migration. Proceed // to the next step. - if (res.left.type === 'index_not_found_exception') { + if (isLeftTypeof(res.left, 'index_not_found_exception')) { return { ...stateP, controlState: 'LEGACY_CREATE_REINDEX_TARGET' }; } else { // @ts-expect-error TS doesn't correctly narrow this type to never @@ -376,8 +362,8 @@ export const model = (currentState: State, resW: ResponseType): } else { const left = res.left; if ( - (left.type === 'index_not_found_exception' && left.index === stateP.legacyIndex) || - left.type === 'target_index_had_write_block' + (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) || + isLeftTypeof(left, 'target_index_had_write_block') ) { // index_not_found_exception for the LEGACY_REINDEX source index: // another instance already complete the LEGACY_DELETE step. @@ -390,12 +376,23 @@ export const model = (currentState: State, resW: ResponseType): // step. However, by not skipping ahead we limit branches in the // control state progression and simplify the implementation. return { ...stateP, controlState: 'LEGACY_DELETE' }; - } else { + } else if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { + // After waiting for the specificed timeout, the task has not yet + // completed. Retry this step to see if the task has completed after an + // exponential delay. We will basically keep polling forever until the + // Elasticeasrch task succeeds or fails. + return delayRetryState(stateP, left.message, Number.MAX_SAFE_INTEGER); + } else if ( + isLeftTypeof(left, 'index_not_found_exception') || + isLeftTypeof(left, 'incompatible_mapping_exception') + ) { // We don't handle the following errors as the algorithm will never // run into these during the LEGACY_REINDEX_WAIT_FOR_TASK step: // - index_not_found_exception for the LEGACY_REINDEX target index - // - strict_dynamic_mapping_exception + // - incompatible_mapping_exception throwBadResponse(stateP, left as never); + } else { + throwBadResponse(stateP, left); } } } else if (stateP.controlState === 'LEGACY_DELETE') { @@ -405,8 +402,8 @@ export const model = (currentState: State, resW: ResponseType): } else if (Either.isLeft(res)) { const left = res.left; if ( - left.type === 'remove_index_not_a_concrete_index' || - (left.type === 'index_not_found_exception' && left.index === stateP.legacyIndex) + isLeftTypeof(left, 'remove_index_not_a_concrete_index') || + (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.legacyIndex) ) { // index_not_found_exception, another Kibana instance already // deleted the legacy index @@ -419,13 +416,18 @@ export const model = (currentState: State, resW: ResponseType): // step. However, by not skipping ahead we limit branches in the // control state progression and simplify the implementation. return { ...stateP, controlState: 'SET_SOURCE_WRITE_BLOCK' }; - } else { + } else if ( + isLeftTypeof(left, 'index_not_found_exception') || + isLeftTypeof(left, 'alias_not_found_exception') + ) { // We don't handle the following errors as the migration algorithm // will never cause them to occur: // - alias_not_found_exception we're not using must_exist // - index_not_found_exception for source index into which we reindex // the legacy index throwBadResponse(stateP, left as never); + } else { + throwBadResponse(stateP, left); } } else { throwBadResponse(stateP, res); @@ -438,11 +440,13 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'CREATE_REINDEX_TEMP', }; - } else { + } else if (isLeftTypeof(res.left, 'index_not_found_exception')) { // We don't handle the following errors as the migration algorithm // will never cause them to occur: // - index_not_found_exception - return throwBadResponse(stateP, res as never); + return throwBadResponse(stateP, res.left as never); + } else { + return throwBadResponse(stateP, res.left); } } else if (stateP.controlState === 'CREATE_REINDEX_TEMP') { const res = resW as ExcludeRetryableEsError>; @@ -477,8 +481,8 @@ export const model = (currentState: State, resW: ResponseType): } else { const left = res.left; if ( - left.type === 'target_index_had_write_block' || - (left.type === 'index_not_found_exception' && left.index === stateP.tempIndex) + isLeftTypeof(left, 'target_index_had_write_block') || + (isLeftTypeof(left, 'index_not_found_exception') && left.index === stateP.tempIndex) ) { // index_not_found_exception: // another instance completed the MARK_VERSION_INDEX_READY and @@ -493,10 +497,25 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'SET_TEMP_WRITE_BLOCK', }; - } else { - // Don't handle incompatible_mapping_exception as we will never add a write - // block to the temp index or change the mappings. + } else if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { + // After waiting for the specificed timeout, the task has not yet + // completed. Retry this step to see if the task has completed after an + // exponential delay. We will basically keep polling forever until the + // Elasticeasrch task succeeds or fails. + return delayRetryState(stateP, left.message, Number.MAX_SAFE_INTEGER); + } else if ( + isLeftTypeof(left, 'index_not_found_exception') || + isLeftTypeof(left, 'incompatible_mapping_exception') + ) { + // Don't handle the following errors as the migration algorithm should + // never cause them to occur: + // - incompatible_mapping_exception the temp index has `dynamic: false` + // mappings + // - index_not_found_exception for the source index, we will never + // delete the source index throwBadResponse(stateP, left as never); + } else { + throwBadResponse(stateP, left); } } } else if (stateP.controlState === 'SET_TEMP_WRITE_BLOCK') { @@ -508,7 +527,7 @@ export const model = (currentState: State, resW: ResponseType): }; } else { const left = res.left; - if (left.type === 'index_not_found_exception') { + if (isLeftTypeof(left, 'index_not_found_exception')) { // index_not_found_exception: // another instance completed the MARK_VERSION_INDEX_READY and // removed the temp index. @@ -520,7 +539,6 @@ export const model = (currentState: State, resW: ResponseType): controlState: 'CLONE_TEMP_TO_TARGET', }; } else { - // @ts-expect-error TS doesn't correctly narrow this to never throwBadResponse(stateP, left); } } @@ -533,7 +551,7 @@ export const model = (currentState: State, resW: ResponseType): }; } else { const left = res.left; - if (left.type === 'index_not_found_exception') { + if (isLeftTypeof(left, 'index_not_found_exception')) { // index_not_found_exception means another instance alread completed // the MARK_VERSION_INDEX_READY step and removed the temp index // We still perform the OUTDATED_DOCUMENTS_* and @@ -543,8 +561,9 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'OUTDATED_DOCUMENTS_SEARCH', }; + } else { + throwBadResponse(stateP, left); } - throwBadResponse(stateP, res as never); } } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH') { const res = resW as ExcludeRetryableEsError>; @@ -611,7 +630,16 @@ export const model = (currentState: State, resW: ResponseType): }; } } else { - throwBadResponse(stateP, res); + const left = res.left; + if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { + // After waiting for the specificed timeout, the task has not yet + // completed. Retry this step to see if the task has completed after an + // exponential delay. We will basically keep polling forever until the + // Elasticeasrch task succeeds or fails. + return delayRetryState(stateP, res.left.message, Number.MAX_SAFE_INTEGER); + } else { + throwBadResponse(stateP, left); + } } } else if (stateP.controlState === 'CREATE_NEW_TARGET') { const res = resW as ExcludeRetryableEsError>; @@ -632,13 +660,13 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'DONE' }; } else { const left = res.left; - if (left.type === 'alias_not_found_exception') { + if (isLeftTypeof(left, 'alias_not_found_exception')) { // the versionIndexReadyActions checks that the currentAlias is still // pointing to the source index. If this fails with an // alias_not_found_exception another instance has completed a // migration from the same source. return { ...stateP, controlState: 'MARK_VERSION_INDEX_READY_CONFLICT' }; - } else if (left.type === 'index_not_found_exception') { + } else if (isLeftTypeof(left, 'index_not_found_exception')) { if (left.index === stateP.tempIndex) { // another instance has already completed the migration and deleted // the temporary index @@ -649,7 +677,7 @@ export const model = (currentState: State, resW: ResponseType): // index handled above. throwBadResponse(stateP, left as never); } - } else if (left.type === 'remove_index_not_a_concrete_index') { + } else if (isLeftTypeof(left, 'remove_index_not_a_concrete_index')) { // We don't handle this error as the migration algorithm will never // cause it to occur (this error is only relevant to the LEGACY_DELETE // step). @@ -708,12 +736,14 @@ export const createInitialState = ({ preMigrationScript, migrationVersionPerType, indexPrefix, + migrationsConfig, }: { kibanaVersion: string; targetMappings: IndexMapping; preMigrationScript?: string; migrationVersionPerType: SavedObjectsMigrationVersion; indexPrefix: string; + migrationsConfig: SavedObjectsMigrationConfigType; }): InitState => { const outdatedDocumentsQuery = { bool: { @@ -753,6 +783,7 @@ export const createInitialState = ({ outdatedDocumentsQuery, retryCount: 0, retryDelay: 0, + retryAttempts: migrationsConfig.retryAttempts, logs: [], }; return initialState; diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts index 6625c446e2282..ebbb540c9b4fd 100644 --- a/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/state_action_machine.test.ts @@ -89,12 +89,4 @@ describe('state action machine', () => { } `); }); - - test("rejects if control state doesn't change after 50 steps", async () => { - await expect( - stateActionMachine(state, next, countUntilModel(51)) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Control state didn't change after 50 steps aborting."` - ); - }); }); diff --git a/src/core/server/saved_objects/migrationsv2/state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/state_action_machine.ts index c5aa4bf7c42c6..b011ab694e145 100644 --- a/src/core/server/saved_objects/migrationsv2/state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/state_action_machine.ts @@ -10,8 +10,6 @@ export interface ControlState { controlState: string; } -const MAX_STEPS_WITHOUT_CONTROL_STATE_CHANGE = 50; - /** * A state-action machine next function that returns the next action thunk * based on the passed in state. @@ -65,7 +63,6 @@ export async function stateActionMachine( model: Model ) { let state = initialState; - let controlStateStepCounter = 0; let nextAction = next(state); while (nextAction != null) { @@ -73,15 +70,6 @@ export async function stateActionMachine( const actionResponse = await nextAction(); const newState = model(state, actionResponse); - controlStateStepCounter = - newState.controlState === state.controlState ? controlStateStepCounter + 1 : 0; - if (controlStateStepCounter >= MAX_STEPS_WITHOUT_CONTROL_STATE_CHANGE) { - // This is just a fail-safe to ensure we don't get stuck in an infinite loop - throw new Error( - `Control state didn't change after ${MAX_STEPS_WITHOUT_CONTROL_STATE_CHANGE} steps aborting.` - ); - } - // Get ready for the next step state = newState; nextAction = next(state); diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index b8d67d04b3334..dbdd5774dfa62 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -37,6 +37,23 @@ export interface BaseState extends ControlState { readonly outdatedDocumentsQuery: Record; readonly retryCount: number; readonly retryDelay: number; + /** + * How many times to retry a step that fails with retryable_es_client_error + * such as a statusCode: 503 or a snapshot_in_progress_exception. + * + * We don't want to immediately crash Kibana and cause a reboot for these + * intermittent. However, if we're still receiving e.g. a 503 after 10 minutes + * this is probably not just a temporary problem so we stop trying and exit + * with a fatal error. + * + * Because of the exponential backoff the total time we will retry such errors + * is: + * max_retry_time = 2+4+8+16+32+64*(RETRY_ATTEMPTS-5) + ACTION_DURATION*RETRY_ATTEMPTS + * + * For RETRY_ATTEMPTS=15 (default), ACTION_DURATION=0 + * max_retry_time = 11.7 minutes + */ + readonly retryAttempts: number; readonly logs: Array<{ level: 'error' | 'info'; message: string }>; /** * The current alias e.g. `.kibana` which always points to the latest diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 1806bb6e0c895..7228cb126d286 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -13,12 +13,14 @@ export type SavedObjectsMigrationConfigType = TypeOf; logQueries: Type; ssl: import("@kbn/config-schema").ObjectType<{ - verificationMode: Type<"certificate" | "none" | "full">; + verificationMode: Type<"none" | "certificate" | "full">; certificateAuthorities: Type; certificate: Type; key: Type; @@ -374,8 +375,6 @@ export { ConfigDeprecation } export { ConfigDeprecationFactory } -export { ConfigDeprecationLogger } - export { ConfigDeprecationProvider } export { ConfigPath } @@ -491,6 +490,8 @@ export interface CoreSetup; @@ -830,12 +831,40 @@ export interface DeprecationInfo { url: string; } +// Warning: (ae-missing-release-tag) "DeprecationsDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface DeprecationsDetails { + // (undocumented) + correctiveActions: { + api?: { + path: string; + method: 'POST' | 'PUT'; + body?: { + [key: string]: any; + }; + }; + manualSteps?: string[]; + }; + // (undocumented) + documentationUrl?: string; + level: 'warning' | 'critical' | 'fetch_error'; + // (undocumented) + message: string; +} + // @public export interface DeprecationSettings { docLinksKey: string; message: string; } +// @public +export interface DeprecationsServiceSetup { + // (undocumented) + registerDeprecations: (deprecationContext: RegisterDeprecationsConfig) => void; +} + // @public export type DestructiveRouteMethod = 'post' | 'put' | 'delete' | 'patch'; @@ -939,6 +968,16 @@ export type GetAuthState = (request: KibanaRequest | LegacyRequest) state: T; }; +// Warning: (ae-missing-release-tag) "GetDeprecationsContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface GetDeprecationsContext { + // (undocumented) + esClient: IScopedClusterClient; + // (undocumented) + savedObjectsClient: SavedObjectsClientContract; +} + // @public (undocumented) export interface GetResponse { // (undocumented) @@ -1265,10 +1304,10 @@ export type KibanaResponseFactory = typeof kibanaResponseFactory; // @public export const kibanaResponseFactory: { - custom: | Error | Buffer | { + custom: | Error | Buffer | Stream | { message: string | Error; attributes?: Record | undefined; - } | Stream | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; + } | undefined>(options: CustomHttpResponseOptions) => KibanaResponse; badRequest: (options?: ErrorHttpResponseOptions) => KibanaResponse; unauthorized: (options?: ErrorHttpResponseOptions) => KibanaResponse; forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; @@ -1545,20 +1584,6 @@ export class LegacyClusterClient implements ILegacyClusterClient { close(): void; } -// @internal @deprecated -export interface LegacyConfig { - // (undocumented) - get(key?: string): T; - // (undocumented) - has(key: string): boolean; - // (undocumented) - set(key: string, value: any): void; - // Warning: (ae-forgotten-export) The symbol "LegacyVars" needs to be exported by the entry point index.d.ts - // - // (undocumented) - set(config: LegacyVars): void; -} - // @public @deprecated (undocumented) export type LegacyElasticsearchClientConfig = Pick & Pick & { pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout']; @@ -1594,30 +1619,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// @public @deprecated (undocumented) -export interface LegacyServiceSetupDeps { - // Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: LegacyCoreSetup; - // (undocumented) - plugins: Record; - // Warning: (ae-forgotten-export) The symbol "UiPlugins" needs to be exported by the entry point index.d.ts - // - // (undocumented) - uiPlugins: UiPlugins; -} - -// @public @deprecated (undocumented) -export interface LegacyServiceStartDeps { - // Warning: (ae-forgotten-export) The symbol "LegacyCoreStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - core: LegacyCoreStart; - // (undocumented) - plugins: Record; -} - // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // // @public @@ -1912,6 +1913,16 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; }; +// Warning: (ae-missing-release-tag) "RegisterDeprecationsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface RegisterDeprecationsConfig { + // Warning: (ae-forgotten-export) The symbol "MaybePromise" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getDeprecations: (context: GetDeprecationsContext) => MaybePromise; +} + // @public export type RequestHandler

= (context: Context, request: KibanaRequest, response: ResponseFactory) => IKibanaResponse | Promise>; @@ -3209,9 +3220,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:286:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:289:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:394:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:293:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:296:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:401:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 96047dc6921ec..2bd3028b2f1b6 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -58,7 +58,7 @@ jest.doMock('./ui_settings/ui_settings_service', () => ({ })); export const mockEnsureValidConfiguration = jest.fn(); -jest.doMock('./legacy/config/ensure_valid_configuration', () => ({ +jest.doMock('./config/ensure_valid_configuration', () => ({ ensureValidConfiguration: mockEnsureValidConfiguration, })); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index fcf09b0295bcb..534d7df9d9466 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -99,7 +99,6 @@ test('injects legacy dependency to context#setup()', async () => { pluginDependencies: new Map([ [pluginA, []], [pluginB, [pluginA]], - [mockLegacyService.legacyId, [pluginA, pluginB]], ]), }); }); @@ -108,12 +107,10 @@ test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.start).not.toHaveBeenCalled(); await server.setup(); expect(mockHttpService.start).not.toHaveBeenCalled(); - expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); @@ -121,7 +118,6 @@ test('runs services on "start"', async () => { await server.start(); expect(mockHttpService.start).toHaveBeenCalledTimes(1); - expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); @@ -164,26 +160,6 @@ test('stops services on "stop"', async () => { }); test(`doesn't setup core services if config validation fails`, async () => { - mockConfigService.validate.mockImplementationOnce(() => { - return Promise.reject(new Error('invalid config')); - }); - const server = new Server(rawConfigService, env, logger); - await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid config"`); - - expect(mockHttpService.setup).not.toHaveBeenCalled(); - expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); - expect(mockPluginsService.setup).not.toHaveBeenCalled(); - expect(mockLegacyService.setup).not.toHaveBeenCalled(); - expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); - expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); - expect(mockRenderingService.setup).not.toHaveBeenCalled(); - expect(mockMetricsService.setup).not.toHaveBeenCalled(); - expect(mockStatusService.setup).not.toHaveBeenCalled(); - expect(mockLoggingService.setup).not.toHaveBeenCalled(); - expect(mockI18nService.setup).not.toHaveBeenCalled(); -}); - -test(`doesn't setup core services if legacy config validation fails`, async () => { mockEnsureValidConfiguration.mockImplementation(() => { throw new Error('Unknown configuration keys'); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 8905bcd28fe17..b34d7fec3dcbf 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -8,15 +8,20 @@ import apm from 'elastic-apm-node'; import { config as pathConfig } from '@kbn/utils'; -import { mapToObject } from '@kbn/std'; -import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; +import { + ConfigService, + Env, + RawConfigurationProvider, + coreDeprecationProvider, + ensureValidConfiguration, +} from './config'; import { CoreApp } from './core_app'; import { I18nService } from './i18n'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; import { RenderingService } from './rendering'; -import { LegacyService, ensureValidConfiguration } from './legacy'; +import { LegacyService } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; @@ -41,6 +46,7 @@ import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; +import { DeprecationsService } from './deprecations'; import { CoreRouteHandlerContext } from './core_route_handler_context'; import { config as externalUrlConfig } from './external_url'; @@ -67,6 +73,7 @@ export class Server { private readonly coreApp: CoreApp; private readonly coreUsageData: CoreUsageDataService; private readonly i18n: I18nService; + private readonly deprecations: DeprecationsService; private readonly savedObjectsStartPromise: Promise; private resolveSavedObjectsStartPromise?: (value: SavedObjectsServiceStart) => void; @@ -102,6 +109,7 @@ export class Server { this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); this.i18n = new I18nService(core); + this.deprecations = new DeprecationsService(core); this.savedObjectsStartPromise = new Promise((resolve) => { this.resolveSavedObjectsStartPromise = resolve; @@ -118,22 +126,13 @@ export class Server { const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); - const legacyConfigSetup = await this.legacy.setupLegacyConfig(); // Immediately terminate in case of invalid configuration // This needs to be done after plugin discovery - await this.configService.validate(); - await ensureValidConfiguration(this.configService, legacyConfigSetup); + await ensureValidConfiguration(this.configService); const contextServiceSetup = this.context.setup({ - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any KP plugin - // 2) Can register context providers that will only be available to other legacy plugins and will not leak into - // New Platform plugins. - pluginDependencies: new Map([ - ...pluginTree.asOpaqueIds, - [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], - ]), + pluginDependencies: new Map([...pluginTree.asOpaqueIds]), }); const httpSetup = await this.http.setup({ @@ -192,6 +191,12 @@ export class Server { loggingSystem: this.loggingSystem, }); + const deprecationsSetup = this.deprecations.setup({ + http: httpSetup, + elasticsearch: elasticsearchServiceSetup, + coreUsageData: coreUsageDataSetup, + }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -206,15 +211,14 @@ export class Server { httpResources: httpResourcesSetup, logging: loggingSetup, metrics: metricsSetup, + deprecations: deprecationsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); this.#pluginsInitialized = pluginsSetup.initialized; await this.legacy.setup({ - core: { ...coreSetup, plugins: pluginsSetup, rendering: renderingSetup }, - plugins: mapToObject(pluginsSetup.contracts), - uiPlugins, + http: httpSetup, }); this.registerCoreContext(coreSetup); @@ -256,15 +260,7 @@ export class Server { coreUsageData: coreUsageDataStart, }; - const pluginsStart = await this.plugins.start(this.coreStart); - - await this.legacy.start({ - core: { - ...this.coreStart, - plugins: pluginsStart, - }, - plugins: mapToObject(pluginsStart.contracts), - }); + await this.plugins.start(this.coreStart); await this.http.start(); @@ -285,6 +281,7 @@ export class Server { await this.metrics.stop(); await this.status.stop(); await this.logging.stop(); + this.deprecations.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 6bd805d55af1d..be07a3cfb1fd3 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -37,7 +37,7 @@ export type { SavedObjectsClientContract, SavedObjectsNamespaceType, } from './saved_objects/types'; +export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types'; export * from './ui_settings/types'; -export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; export type { ExternalUrlConfig, IExternalUrlPolicy } from './external_url'; diff --git a/src/core/server/ui_settings/integration_tests/doc_exists.ts b/src/core/server/ui_settings/integration_tests/doc_exists.ts index 86a9a24fab6de..59c27cc136174 100644 --- a/src/core/server/ui_settings/integration_tests/doc_exists.ts +++ b/src/core/server/ui_settings/integration_tests/doc_exists.ts @@ -9,10 +9,10 @@ import { getServices, chance } from './lib'; export const docExistsSuite = (savedObjectsIndex: string) => () => { - async function setup(options: any = {}) { + async function setup(options: { initialSettings?: Record } = {}) { const { initialSettings } = options; - const { kbnServer, uiSettings, callCluster } = getServices(); + const { uiSettings, callCluster, supertest } = getServices(); // delete the kibana index to ensure we start fresh await callCluster('deleteByQuery', { @@ -21,31 +21,30 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { conflicts: 'proceed', query: { match_all: {} }, }, + refresh: true, + wait_for_completion: true, }); if (initialSettings) { await uiSettings.setMany(initialSettings); } - return { kbnServer, uiSettings }; + return { uiSettings, supertest }; } describe('get route', () => { it('returns a 200 and includes userValues', async () => { const defaultIndex = chance.word({ length: 10 }); - const { kbnServer } = await setup({ + + const { supertest } = await setup({ initialSettings: { defaultIndex, }, }); - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); + const { body } = await supertest('get', '/api/kibana/settings').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -64,20 +63,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { describe('set route', () => { it('returns a 200 and all values including update', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { - value: defaultIndex, - }, - }); - expect(statusCode).toBe(200); + const { body } = await supertest('post', '/api/kibana/settings/defaultIndex') + .send({ + value: defaultIndex, + }) + .expect(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -94,18 +90,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); it('returns a 400 if trying to set overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/foo', - payload: { + const { body } = await supertest('delete', '/api/kibana/settings/foo') + .send({ value: 'baz', - }, - }); + }) + .expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -115,22 +108,18 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { describe('setMany route', () => { it('returns a 200 and all values including updates', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { defaultIndex, }, - }, - }); + }) + .expect(200); - expect(statusCode).toBe(200); - - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -147,20 +136,17 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); it('returns a 400 if trying to set overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { foo: 'baz', }, - }, - }); + }) + .expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, @@ -172,19 +158,15 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { it('returns a 200 and deletes the setting', async () => { const defaultIndex = chance.word({ length: 10 }); - const { kbnServer, uiSettings } = await setup({ + const { uiSettings, supertest } = await setup({ initialSettings: { defaultIndex }, }); expect(await uiSettings.get('defaultIndex')).toBe(defaultIndex); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); + const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -197,15 +179,11 @@ export const docExistsSuite = (savedObjectsIndex: string) => () => { }); }); it('returns a 400 if deleting overridden value', async () => { - const { kbnServer } = await setup(); + const { supertest } = await setup(); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/foo', - }); + const { body } = await supertest('delete', '/api/kibana/settings/foo').expect(400); - expect(statusCode).toBe(400); - expect(result).toEqual({ + expect(body).toEqual({ error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', statusCode: 400, diff --git a/src/core/server/ui_settings/integration_tests/doc_missing.ts b/src/core/server/ui_settings/integration_tests/doc_missing.ts index 9fa3e4c1cfe78..29d1daf3b2032 100644 --- a/src/core/server/ui_settings/integration_tests/doc_missing.ts +++ b/src/core/server/ui_settings/integration_tests/doc_missing.ts @@ -11,14 +11,7 @@ import { getServices, chance } from './lib'; export const docMissingSuite = (savedObjectsIndex: string) => () => { // ensure the kibana index has no documents beforeEach(async () => { - const { kbnServer, callCluster } = getServices(); - - // write a setting to ensure kibana index is created - await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' }, - }); + const { callCluster } = getServices(); // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { @@ -31,15 +24,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('get route', () => { it('creates doc, returns a 200 with settings', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); + const { body } = await supertest('get', '/api/kibana/settings').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -55,17 +44,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('set route', () => { it('creates doc, returns a 200 with value set', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex }, - }); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + const { body } = await supertest('post', '/api/kibana/settings/defaultIndex') + .send({ + value: defaultIndex, + }) + .expect(200); + + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -84,19 +73,17 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('setMany route', () => { it('creates doc, returns 200 with updated values', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { + + const { body } = await supertest('post', '/api/kibana/settings') + .send({ changes: { defaultIndex }, - }, - }); + }) + .expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), @@ -115,15 +102,11 @@ export const docMissingSuite = (savedObjectsIndex: string) => () => { describe('delete route', () => { it('creates doc, returns a 200 with just buildNum', async () => { - const { kbnServer } = getServices(); + const { supertest } = getServices(); - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); + const { body } = await supertest('delete', '/api/kibana/settings/defaultIndex').expect(200); - expect(statusCode).toBe(200); - expect(result).toMatchObject({ + expect(body).toMatchObject({ settings: { buildNum: { userValue: expect.any(Number), diff --git a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts b/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts deleted file mode 100644 index 78fdab7eb8c5d..0000000000000 --- a/src/core/server/ui_settings/integration_tests/doc_missing_and_index_read_only.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getServices, chance } from './lib'; - -export const docMissingAndIndexReadOnlySuite = (savedObjectsIndex: string) => () => { - // ensure the kibana index has no documents - beforeEach(async () => { - const { kbnServer, callCluster } = getServices(); - - // write a setting to ensure kibana index is created - await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' }, - }); - - // delete all docs from kibana index to ensure savedConfig is not found - await callCluster('deleteByQuery', { - index: savedObjectsIndex, - body: { - query: { match_all: {} }, - }, - }); - - // set the index to read only - await callCluster('indices.putSettings', { - index: savedObjectsIndex, - body: { - index: { - blocks: { - read_only: true, - }, - }, - }, - }); - }); - - afterEach(async () => { - const { callCluster } = getServices(); - - // disable the read only block - await callCluster('indices.putSettings', { - index: savedObjectsIndex, - body: { - index: { - blocks: { - read_only: false, - }, - }, - }, - }); - }); - - describe('get route', () => { - it('returns simulated doc with buildNum', async () => { - const { kbnServer } = getServices(); - - const { statusCode, result } = await kbnServer.inject({ - method: 'GET', - url: '/api/kibana/settings', - }); - - expect(statusCode).toBe(200); - - expect(result).toMatchObject({ - settings: { - buildNum: { - userValue: expect.any(Number), - }, - foo: { - userValue: 'bar', - isOverridden: true, - }, - }, - }); - }); - }); - - describe('set route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex }, - }); - - expect(statusCode).toBe(403); - - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); - - describe('setMany route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const defaultIndex = chance.word(); - const { statusCode, result } = await kbnServer.inject({ - method: 'POST', - url: '/api/kibana/settings', - payload: { - changes: { defaultIndex }, - }, - }); - - expect(statusCode).toBe(403); - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); - - describe('delete route', () => { - it('fails with 403 forbidden', async () => { - const { kbnServer } = getServices(); - - const { statusCode, result } = await kbnServer.inject({ - method: 'DELETE', - url: '/api/kibana/settings/defaultIndex', - }); - - expect(statusCode).toBe(403); - expect(result).toEqual({ - error: 'Forbidden', - message: expect.stringContaining('index read-only'), - statusCode: 403, - }); - }); - }); -}; 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 6e6c357e6cccc..6c7cdfa43cf57 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -12,7 +12,6 @@ import { getEnvOptions } from '@kbn/config/target/mocks'; import { startServers, stopServers } from './lib'; import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; -import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; const savedObjectIndex = `.kibana_${kibanaVersion}_001`; @@ -23,7 +22,6 @@ describe('uiSettings/routes', function () { beforeAll(startServers); /* eslint-disable jest/valid-describe */ describe('doc missing', docMissingSuite(savedObjectIndex)); - describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite(savedObjectIndex)); describe('doc exists', docExistsSuite(savedObjectIndex)); /* eslint-enable jest/valid-describe */ afterAll(stopServers); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 87176bed5de11..d019dc640f385 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type supertest from 'supertest'; import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server'; import { @@ -13,6 +14,8 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, + HttpMethod, + getSupertest, } from '../../../../test_helpers/kbn_server'; import { LegacyAPICaller } from '../../../elasticsearch/'; import { httpServerMock } from '../../../http/http_server.mocks'; @@ -21,13 +24,11 @@ let servers: TestUtils; let esServer: TestElasticsearchUtils; let kbn: TestKibanaUtils; -let kbnServer: TestKibanaUtils['kbnServer']; - interface AllServices { - kbnServer: TestKibanaUtils['kbnServer']; savedObjectsClient: SavedObjectsClientContract; callCluster: LegacyAPICaller; uiSettings: IUiSettingsClient; + supertest: (method: HttpMethod, path: string) => supertest.Test; } let services: AllServices; @@ -47,7 +48,6 @@ export async function startServers() { }); esServer = await servers.startES(); kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; } export function getServices() { @@ -61,12 +61,10 @@ export function getServices() { httpServerMock.createKibanaRequest() ); - const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( - savedObjectsClient - ); + const uiSettings = kbn.coreStart.uiSettings.asScopedToClient(savedObjectsClient); services = { - kbnServer, + supertest: (method: HttpMethod, path: string) => getSupertest(kbn.root, method, path), callCluster, savedObjectsClient, uiSettings, @@ -77,7 +75,6 @@ export function getServices() { export async function stopServers() { services = null!; - kbnServer = null!; if (servers) { await esServer.stop(); await kbn.stop(); diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index d702fed73778f..950ab5f4392e1 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -9,12 +9,19 @@ import { Client } from 'elasticsearch'; import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; import { + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 createLegacyEsTestCluster, + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 DEFAULT_SUPERUSER_PASS, + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 esTestConfig, + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 kbnTestConfig, + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 kibanaServerTestUser, + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 kibanaTestUser, + // @ts-expect-error https://github.com/elastic/kibana/issues/95679 setupUsers, } from '@kbn/test'; import { defaultsDeep, get } from 'lodash'; @@ -22,11 +29,10 @@ import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; -import { CoreStart } from 'src/core/server'; +import { InternalCoreSetup, InternalCoreStart } from '../server/internal_types'; import { LegacyAPICaller } from '../server/elasticsearch'; import { CliArgs, Env } from '../server/config'; import { Root } from '../server/root'; -import KbnServer from '../../legacy/server/kbn_server'; export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -118,14 +124,6 @@ export function createRootWithCorePlugins(settings = {}, cliArgs: Partial ReturnType @@ -157,8 +155,8 @@ export interface TestElasticsearchUtils { export interface TestKibanaUtils { root: Root; - coreStart: CoreStart; - kbnServer: KbnServer; + coreSetup: InternalCoreSetup; + coreStart: InternalCoreStart; stop: () => Promise; } @@ -276,14 +274,12 @@ export function createTestServers({ startKibana: async () => { const root = createRootWithCorePlugins(kbnSettings); - await root.setup(); + const coreSetup = await root.setup(); const coreStart = await root.start(); - const kbnServer = getKbnServer(root); - return { root, - kbnServer, + coreSetup, coreStart, stop: async () => await root.shutdown(), }; diff --git a/src/dev/build/tasks/bin/scripts/kibana b/src/dev/build/tasks/bin/scripts/kibana index 3c12c8bbf58d0..a4fc5385500b5 100755 --- a/src/dev/build/tasks/bin/scripts/kibana +++ b/src/dev/build/tasks/bin/scripts/kibana @@ -26,4 +26,4 @@ if [ -f "${CONFIG_DIR}/node.options" ]; then KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)" fi -NODE_OPTIONS="--no-warnings --max-http-header-size=65536 --tls-min-v1.0 $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli/dist" ${@} +NODE_OPTIONS="--no-warnings --max-http-header-size=65536 $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli/dist" ${@} diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index e37a61582c6a8..2ae882000cae0 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -49,6 +49,7 @@ export const CreateRpmPackage: Task = { }, }; +const dockerBuildDate = new Date().toISOString(); export const CreateDockerCentOS: Task = { description: 'Creating Docker CentOS image', @@ -57,11 +58,13 @@ export const CreateDockerCentOS: Task = { architecture: 'x64', context: false, image: true, + dockerBuildDate, }); await runDockerGenerator(config, log, build, { architecture: 'aarch64', context: false, image: true, + dockerBuildDate, }); }, }; @@ -76,6 +79,7 @@ export const CreateDockerUBI: Task = { context: false, ubi: true, image: true, + dockerBuildDate, }); } }, @@ -88,6 +92,7 @@ export const CreateDockerContexts: Task = { await runDockerGenerator(config, log, build, { context: true, image: false, + dockerBuildDate, }); if (!build.isOss()) { @@ -95,11 +100,13 @@ export const CreateDockerContexts: Task = { ubi: true, context: true, image: false, + dockerBuildDate, }); await runDockerGenerator(config, log, build, { ironbank: true, context: true, image: false, + dockerBuildDate, }); } }, 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 8bf876b558431..c72112b7b6b03 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -33,6 +33,7 @@ export async function runDockerGenerator( image: boolean; ubi?: boolean; ironbank?: boolean; + dockerBuildDate?: string; } ) { // UBI var config @@ -53,7 +54,7 @@ export async function runDockerGenerator( const artifactPrefix = `kibana${artifactFlavor}-${version}-linux`; const artifactTarball = `${artifactPrefix}-${artifactArchitecture}.tar.gz`; const artifactsDir = config.resolveFromTarget('.'); - const dockerBuildDate = new Date().toISOString(); + const dockerBuildDate = flags.dockerBuildDate || new Date().toISOString(); // That would produce oss, default and default-ubi7 const dockerBuildDir = config.resolveFromRepo( 'build', diff --git a/src/dev/run_check_file_casing.js b/src/dev/run_check_file_casing.ts similarity index 92% rename from src/dev/run_check_file_casing.js rename to src/dev/run_check_file_casing.ts index 0add66dd272c8..554aa2418f579 100644 --- a/src/dev/run_check_file_casing.js +++ b/src/dev/run_check_file_casing.ts @@ -11,12 +11,13 @@ import globby from 'globby'; import { REPO_ROOT } from '@kbn/utils'; import { run } from '@kbn/dev-utils'; import { File } from './file'; +// @ts-expect-error precommit hooks aren't migrated to TypeScript yet. import { checkFileCasing } from './precommit_hook/check_file_casing'; run(async ({ log }) => { const paths = await globby('**/*', { cwd: REPO_ROOT, - nodir: true, + onlyFiles: true, gitignore: true, ignore: [ // the gitignore: true option makes sure that we don't diff --git a/src/legacy/server/config/__snapshots__/config.test.js.snap b/src/legacy/server/config/__snapshots__/config.test.js.snap deleted file mode 100644 index 3bf471f8aba20..0000000000000 --- a/src/legacy/server/config/__snapshots__/config.test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`lib/config/config class Config() #getDefault(key) array key should throw exception for unknown key 1`] = `"Unknown config key: foo,bar."`; - -exports[`lib/config/config class Config() #getDefault(key) dot notation key should throw exception for unknown key 1`] = `"Unknown config key: foo.bar."`; diff --git a/src/legacy/server/config/config.js b/src/legacy/server/config/config.js deleted file mode 100644 index 81cb0a36333bd..0000000000000 --- a/src/legacy/server/config/config.js +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Joi from 'joi'; -import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; -import { override } from './override'; -import createDefaultSchema from './schema'; -import { unset, deepCloneWithBuffers as clone, IS_KIBANA_DISTRIBUTABLE } from '../../utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { pkg } from '../../../core/server/utils'; -const schema = Symbol('Joi Schema'); -const schemaExts = Symbol('Schema Extensions'); -const vals = Symbol('config values'); - -export class Config { - static withDefaultSchema(settings = {}) { - const defaultSchema = createDefaultSchema(); - return new Config(defaultSchema, settings); - } - - constructor(initialSchema, initialSettings) { - this[schemaExts] = Object.create(null); - this[vals] = Object.create(null); - - this.extendSchema(initialSchema, initialSettings); - } - - extendSchema(extension, settings, key) { - if (!extension) { - return; - } - - if (!key) { - return _.each(extension._inner.children, (child) => { - this.extendSchema(child.schema, _.get(settings, child.key), child.key); - }); - } - - if (this.has(key)) { - throw new Error(`Config schema already has key: ${key}`); - } - - set(this[schemaExts], key, extension); - this[schema] = null; - - this.set(key, settings); - } - - removeSchema(key) { - if (!_.has(this[schemaExts], key)) { - throw new TypeError(`Unknown schema key: ${key}`); - } - - this[schema] = null; - unset(this[schemaExts], key); - unset(this[vals], key); - } - - resetTo(obj) { - this._commit(obj); - } - - set(key, value) { - // clone and modify the config - let config = clone(this[vals]); - if (_.isPlainObject(key)) { - config = override(config, key); - } else { - set(config, key, value); - } - - // attempt to validate the config value - this._commit(config); - } - - _commit(newVals) { - // resolve the current environment - let env = newVals.env; - delete newVals.env; - if (_.isObject(env)) env = env.name; - if (!env) env = 'production'; - - const dev = env === 'development'; - const prod = env === 'production'; - - // pass the environment as context so that it can be refed in config - const context = { - env: env, - prod: prod, - dev: dev, - notProd: !prod, - notDev: !dev, - version: _.get(pkg, 'version'), - branch: _.get(pkg, 'branch'), - buildNum: IS_KIBANA_DISTRIBUTABLE ? pkg.build.number : Number.MAX_SAFE_INTEGER, - buildSha: IS_KIBANA_DISTRIBUTABLE - ? pkg.build.sha - : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - dist: IS_KIBANA_DISTRIBUTABLE, - }; - - if (!context.dev && !context.prod) { - throw new TypeError( - `Unexpected environment "${env}", expected one of "development" or "production"` - ); - } - - const results = Joi.validate(newVals, this.getSchema(), { - context, - abortEarly: false, - }); - - if (results.error) { - const error = new Error(results.error.message); - error.name = results.error.name; - error.stack = results.error.stack; - throw error; - } - - this[vals] = results.value; - } - - get(key) { - if (!key) { - return clone(this[vals]); - } - - const value = _.get(this[vals], key); - if (value === undefined) { - if (!this.has(key)) { - throw new Error('Unknown config key: ' + key); - } - } - return clone(value); - } - - getDefault(key) { - const schemaKey = Array.isArray(key) ? key.join('.') : key; - - const subSchema = Joi.reach(this.getSchema(), schemaKey); - if (!subSchema) { - throw new Error(`Unknown config key: ${key}.`); - } - - return clone(_.get(Joi.describe(subSchema), 'flags.default')); - } - - has(key) { - function has(key, schema, path) { - path = path || []; - // Catch the partial paths - if (path.join('.') === key) return true; - // Only go deep on inner objects with children - if (_.size(schema._inner.children)) { - for (let i = 0; i < schema._inner.children.length; i++) { - const child = schema._inner.children[i]; - // If the child is an object recurse through it's children and return - // true if there's a match - if (child.schema._type === 'object') { - if (has(key, child.schema, path.concat([child.key]))) return true; - // if the child matches, return true - } else if (path.concat([child.key]).join('.') === key) { - return true; - } - } - } - } - - if (Array.isArray(key)) { - // TODO: add .has() support for array keys - key = key.join('.'); - } - - return !!has(key, this.getSchema()); - } - - getSchema() { - if (!this[schema]) { - this[schema] = (function convertToSchema(children) { - let schema = Joi.object().keys({}).default(); - - for (const key of Object.keys(children)) { - const child = children[key]; - const childSchema = _.isPlainObject(child) ? convertToSchema(child) : child; - - if (!childSchema || !childSchema.isJoi) { - throw new TypeError( - 'Unable to convert configuration definition value to Joi schema: ' + childSchema - ); - } - - schema = schema.keys({ [key]: childSchema }); - } - - return schema; - })(this[schemaExts]); - } - - return this[schema]; - } -} diff --git a/src/legacy/server/config/config.test.js b/src/legacy/server/config/config.test.js deleted file mode 100644 index b617babb8262d..0000000000000 --- a/src/legacy/server/config/config.test.js +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Config } from './config'; -import _ from 'lodash'; -import Joi from 'joi'; - -/** - * Plugins should defined a config method that takes a joi object. By default - * it should return a way to disallow config - * - * Config should be newed up with a joi schema (containing defaults via joi) - * - * let schema = { ... } - * new Config(schema); - * - */ - -const data = { - test: { - hosts: ['host-01', 'host-02'], - client: { - type: 'datastore', - host: 'store-01', - port: 5050, - }, - }, -}; - -const schema = Joi.object({ - test: Joi.object({ - enable: Joi.boolean().default(true), - hosts: Joi.array().items(Joi.string()), - client: Joi.object({ - type: Joi.string().default('datastore'), - host: Joi.string(), - port: Joi.number(), - }).default(), - undefValue: Joi.string(), - }).default(), -}).default(); - -describe('lib/config/config', function () { - describe('class Config()', function () { - describe('constructor', function () { - it('should not allow any config if the schema is not passed', function () { - const config = new Config(); - const run = function () { - config.set('something.enable', true); - }; - expect(run).toThrow(); - }); - - it('should allow keys in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('test.client.host', 'http://localhost'); - }; - expect(run).not.toThrow(); - }); - - it('should not allow keys not in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('paramNotDefinedInTheSchema', true); - }; - expect(run).toThrow(); - }); - - it('should not allow child keys not in the schema', function () { - const config = new Config(schema); - const run = function () { - config.set('test.client.paramNotDefinedInTheSchema', true); - }; - expect(run).toThrow(); - }); - - it('should set defaults', function () { - const config = new Config(schema); - expect(config.get('test.enable')).toBe(true); - expect(config.get('test.client.type')).toBe('datastore'); - }); - }); - - describe('#resetTo(object)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should reset the config object with new values', function () { - config.set(data); - const newData = config.get(); - newData.test.enable = false; - config.resetTo(newData); - expect(config.get()).toEqual(newData); - }); - }); - - describe('#has(key)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should return true for fields that exist in the schema', function () { - expect(config.has('test.undefValue')).toBe(true); - }); - - it('should return true for partial objects that exist in the schema', function () { - expect(config.has('test.client')).toBe(true); - }); - - it('should return false for fields that do not exist in the schema', function () { - expect(config.has('test.client.pool')).toBe(false); - }); - }); - - describe('#set(key, value)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - }); - - it('should use a key and value to set a config value', function () { - config.set('test.enable', false); - expect(config.get('test.enable')).toBe(false); - }); - - it('should use an object to set config values', function () { - const hosts = ['host-01', 'host-02']; - config.set({ test: { enable: false, hosts: hosts } }); - expect(config.get('test.enable')).toBe(false); - expect(config.get('test.hosts')).toEqual(hosts); - }); - - it('should use a flatten object to set config values', function () { - const hosts = ['host-01', 'host-02']; - config.set({ 'test.enable': false, 'test.hosts': hosts }); - expect(config.get('test.enable')).toBe(false); - expect(config.get('test.hosts')).toEqual(hosts); - }); - - it('should override values with just the values present', function () { - const newData = _.cloneDeep(data); - config.set(data); - newData.test.enable = false; - config.set({ test: { enable: false } }); - expect(config.get()).toEqual(newData); - }); - - it('should thow an exception when setting a value with the wrong type', function (done) { - expect.assertions(4); - - const run = function () { - config.set('test.enable', 'something'); - }; - - try { - run(); - } catch (err) { - expect(err).toHaveProperty('name', 'ValidationError'); - expect(err).toHaveProperty( - 'message', - 'child "test" fails because [child "enable" fails because ["enable" must be a boolean]]' - ); - expect(err).not.toHaveProperty('details'); - expect(err).not.toHaveProperty('_object'); - } - - done(); - }); - }); - - describe('#get(key)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - config.set(data); - }); - - it('should return the whole config object when called without a key', function () { - const newData = _.cloneDeep(data); - newData.test.enable = true; - expect(config.get()).toEqual(newData); - }); - - it('should return the value using dot notation', function () { - expect(config.get('test.enable')).toBe(true); - }); - - it('should return the clone of partial object using dot notation', function () { - expect(config.get('test.client')).not.toBe(data.test.client); - expect(config.get('test.client')).toEqual(data.test.client); - }); - - it('should throw exception for unknown config values', function () { - const run = function () { - config.get('test.does.not.exist'); - }; - expect(run).toThrowError(/Unknown config key: test.does.not.exist/); - }); - - it('should not throw exception for undefined known config values', function () { - const run = function getUndefValue() { - config.get('test.undefValue'); - }; - expect(run).not.toThrow(); - }); - }); - - describe('#getDefault(key)', function () { - let config; - - beforeEach(function () { - config = new Config(schema); - config.set(data); - }); - - describe('dot notation key', function () { - it('should return undefined if there is no default', function () { - const hostDefault = config.getDefault('test.client.host'); - expect(hostDefault).toBeUndefined(); - }); - - it('should return default if specified', function () { - const typeDefault = config.getDefault('test.client.type'); - expect(typeDefault).toBe('datastore'); - }); - - it('should throw exception for unknown key', function () { - expect(() => { - config.getDefault('foo.bar'); - }).toThrowErrorMatchingSnapshot(); - }); - }); - - describe('array key', function () { - it('should return undefined if there is no default', function () { - const hostDefault = config.getDefault(['test', 'client', 'host']); - expect(hostDefault).toBeUndefined(); - }); - - it('should return default if specified', function () { - const typeDefault = config.getDefault(['test', 'client', 'type']); - expect(typeDefault).toBe('datastore'); - }); - - it('should throw exception for unknown key', function () { - expect(() => { - config.getDefault(['foo', 'bar']); - }).toThrowErrorMatchingSnapshot(); - }); - }); - - it('object schema with no default should return default value for property', function () { - const noDefaultSchema = Joi.object() - .keys({ - foo: Joi.array().items(Joi.string().min(1)).default(['bar']), - }) - .required(); - - const config = new Config(noDefaultSchema); - config.set({ - foo: ['baz'], - }); - - const fooDefault = config.getDefault('foo'); - expect(fooDefault).toEqual(['bar']); - }); - - it('should return clone of the default', function () { - const schemaWithArrayDefault = Joi.object() - .keys({ - foo: Joi.array().items(Joi.string().min(1)).default(['bar']), - }) - .default(); - - const config = new Config(schemaWithArrayDefault); - config.set({ - foo: ['baz'], - }); - - expect(config.getDefault('foo')).not.toBe(config.getDefault('foo')); - expect(config.getDefault('foo')).toEqual(config.getDefault('foo')); - }); - }); - - describe('#extendSchema(key, schema)', function () { - let config; - beforeEach(function () { - config = new Config(schema); - }); - - it('should allow you to extend the schema at the top level', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema(newSchema, {}, 'myTest'); - expect(config.get('myTest.test')).toBe(true); - }); - - it('should allow you to extend the schema with a prefix', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - config.extendSchema(newSchema, {}, 'prefix.myTest'); - expect(config.get('prefix')).toEqual({ myTest: { test: true } }); - expect(config.get('prefix.myTest')).toEqual({ test: true }); - expect(config.get('prefix.myTest.test')).toBe(true); - }); - - it('should NOT allow you to extend the schema if something else is there', function () { - const newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - const run = function () { - config.extendSchema('test', newSchema); - }; - expect(run).toThrow(); - }); - }); - - describe('#removeSchema(key)', function () { - it('should completely remove the key', function () { - const config = new Config( - Joi.object().keys({ - a: Joi.number().default(1), - }) - ); - - expect(config.get('a')).toBe(1); - config.removeSchema('a'); - expect(() => config.get('a')).toThrowError('Unknown config key'); - }); - - it('only removes existing keys', function () { - const config = new Config(Joi.object()); - - expect(() => config.removeSchema('b')).toThrowError('Unknown schema'); - }); - }); - }); -}); diff --git a/src/legacy/server/config/index.js b/src/legacy/server/config/index.js deleted file mode 100644 index 6fb77eb2a3777..0000000000000 --- a/src/legacy/server/config/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { Config } from './config'; diff --git a/src/legacy/server/config/override.test.ts b/src/legacy/server/config/override.test.ts deleted file mode 100644 index d3046eb7bc8af..0000000000000 --- a/src/legacy/server/config/override.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { override } from './override'; - -describe('override(target, source)', function () { - it('should override the values form source to target', function () { - const target = { - test: { - enable: true, - host: ['something else'], - client: { - type: 'sql', - }, - }, - }; - - const source = { - test: { - host: ['host-01', 'host-02'], - client: { - type: 'nosql', - }, - foo: { - bar: { - baz: 1, - }, - }, - }, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "test": Object { - "client": Object { - "type": "nosql", - }, - "enable": true, - "foo": Object { - "bar": Object { - "baz": 1, - }, - }, - "host": Array [ - "host-01", - "host-02", - ], - }, - } - `); - }); - - it('does not mutate arguments', () => { - const target = { - foo: { - bar: 1, - baz: 1, - }, - }; - - const source = { - foo: { - bar: 2, - }, - box: 2, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "box": 2, - "foo": Object { - "bar": 2, - "baz": 1, - }, - } - `); - expect(target).not.toHaveProperty('box'); - expect(source.foo).not.toHaveProperty('baz'); - }); - - it('explodes keys with dots in them', () => { - const target = { - foo: { - bar: 1, - }, - 'baz.box.boot.bar.bar': 20, - }; - - const source = { - 'foo.bar': 2, - 'baz.box.boot': { - 'bar.foo': 10, - }, - }; - - expect(override(target, source)).toMatchInlineSnapshot(` - Object { - "baz": Object { - "box": Object { - "boot": Object { - "bar": Object { - "bar": 20, - "foo": 10, - }, - }, - }, - }, - "foo": Object { - "bar": 2, - }, - } - `); - }); -}); diff --git a/src/legacy/server/config/override.ts b/src/legacy/server/config/override.ts deleted file mode 100644 index 55147c955539e..0000000000000 --- a/src/legacy/server/config/override.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const isObject = (v: any): v is Record => - typeof v === 'object' && v !== null && !Array.isArray(v); - -const assignDeep = (target: Record, source: Record) => { - for (let [key, value] of Object.entries(source)) { - // unwrap dot-separated keys - if (key.includes('.')) { - const [first, ...others] = key.split('.'); - key = first; - value = { [others.join('.')]: value }; - } - - if (isObject(value)) { - if (!target.hasOwnProperty(key)) { - target[key] = {}; - } - - assignDeep(target[key], value); - } else { - target[key] = value; - } - } -}; - -export const override = (...sources: Array>): Record => { - const result = {}; - - for (const object of sources) { - assignDeep(result, object); - } - - return result; -}; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js deleted file mode 100644 index 81fdfe04290d5..0000000000000 --- a/src/legacy/server/config/schema.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Joi from 'joi'; -import os from 'os'; -import { legacyLoggingConfigSchema } from '@kbn/legacy-logging'; - -const HANDLED_IN_NEW_PLATFORM = Joi.any().description( - 'This key is handled in the new platform ONLY' -); -export default () => - Joi.object({ - elastic: Joi.object({ - apm: HANDLED_IN_NEW_PLATFORM, - }).default(), - - pkg: Joi.object({ - version: Joi.string().default(Joi.ref('$version')), - branch: Joi.string().default(Joi.ref('$branch')), - buildNum: Joi.number().default(Joi.ref('$buildNum')), - buildSha: Joi.string().default(Joi.ref('$buildSha')), - }).default(), - - env: Joi.object({ - name: Joi.string().default(Joi.ref('$env')), - dev: Joi.boolean().default(Joi.ref('$dev')), - prod: Joi.boolean().default(Joi.ref('$prod')), - }).default(), - - dev: HANDLED_IN_NEW_PLATFORM, - pid: HANDLED_IN_NEW_PLATFORM, - csp: HANDLED_IN_NEW_PLATFORM, - - server: Joi.object({ - name: Joi.string().default(os.hostname()), - // keep them for BWC, remove when not used in Legacy. - // validation should be in sync with one in New platform. - // https://github.com/elastic/kibana/blob/master/src/core/server/http/http_config.ts - basePath: Joi.string() - .default('') - .allow('') - .regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), - host: Joi.string().hostname().default('localhost'), - port: Joi.number().default(5601), - rewriteBasePath: Joi.boolean().when('basePath', { - is: '', - then: Joi.default(false).valid(false), - otherwise: Joi.default(false), - }), - - autoListen: HANDLED_IN_NEW_PLATFORM, - cors: HANDLED_IN_NEW_PLATFORM, - customResponseHeaders: HANDLED_IN_NEW_PLATFORM, - keepaliveTimeout: HANDLED_IN_NEW_PLATFORM, - maxPayloadBytes: HANDLED_IN_NEW_PLATFORM, - publicBaseUrl: HANDLED_IN_NEW_PLATFORM, - socketTimeout: HANDLED_IN_NEW_PLATFORM, - ssl: HANDLED_IN_NEW_PLATFORM, - compression: HANDLED_IN_NEW_PLATFORM, - uuid: HANDLED_IN_NEW_PLATFORM, - xsrf: HANDLED_IN_NEW_PLATFORM, - }).default(), - - uiSettings: HANDLED_IN_NEW_PLATFORM, - - logging: legacyLoggingConfigSchema, - - ops: Joi.object({ - interval: Joi.number().default(5000), - cGroupOverrides: HANDLED_IN_NEW_PLATFORM, - }).default(), - - plugins: HANDLED_IN_NEW_PLATFORM, - path: HANDLED_IN_NEW_PLATFORM, - stats: HANDLED_IN_NEW_PLATFORM, - status: HANDLED_IN_NEW_PLATFORM, - map: HANDLED_IN_NEW_PLATFORM, - i18n: HANDLED_IN_NEW_PLATFORM, - - // temporarily moved here from the (now deleted) kibana legacy plugin - kibana: Joi.object({ - enabled: Joi.boolean().default(true), - index: Joi.string().default('.kibana'), - autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), - // TODO Also allow units here like in elasticsearch config once this is moved to the new platform - autocompleteTimeout: Joi.number().integer().min(1).default(1000), - }).default(), - - savedObjects: HANDLED_IN_NEW_PLATFORM, - }).default(); diff --git a/src/legacy/server/config/schema.test.js b/src/legacy/server/config/schema.test.js deleted file mode 100644 index c57e6cf9a933a..0000000000000 --- a/src/legacy/server/config/schema.test.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import schemaProvider from './schema'; -import Joi from 'joi'; - -describe('Config schema', function () { - let schema; - beforeEach(async () => (schema = await schemaProvider())); - - function validate(data, options) { - return Joi.validate(data, schema, options); - } - - describe('server', function () { - it('everything is optional', function () { - const { error } = validate({}); - expect(error).toBe(null); - }); - - describe('basePath', function () { - it('accepts empty strings', function () { - const { error, value } = validate({ server: { basePath: '' } }); - expect(error).toBe(null); - expect(value.server.basePath).toBe(''); - }); - - it('accepts strings with leading slashes', function () { - const { error, value } = validate({ server: { basePath: '/path' } }); - expect(error).toBe(null); - expect(value.server.basePath).toBe('/path'); - }); - - it('rejects strings with trailing slashes', function () { - const { error } = validate({ server: { basePath: '/path/' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - }); - - it('rejects strings without leading slashes', function () { - const { error } = validate({ server: { basePath: 'path' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - }); - - it('rejects things that are not strings', function () { - for (const value of [1, true, {}, [], /foo/]) { - const { error } = validate({ server: { basePath: value } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'basePath']); - } - }); - }); - - describe('rewriteBasePath', function () { - it('defaults to false', () => { - const { error, value } = validate({}); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(false); - }); - - it('accepts false', function () { - const { error, value } = validate({ server: { rewriteBasePath: false } }); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(false); - }); - - it('accepts true if basePath set', function () { - const { error, value } = validate({ server: { basePath: '/foo', rewriteBasePath: true } }); - expect(error).toBe(null); - expect(value.server.rewriteBasePath).toBe(true); - }); - - it('rejects true if basePath not set', function () { - const { error } = validate({ server: { rewriteBasePath: true } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']); - }); - - it('rejects strings', function () { - const { error } = validate({ server: { rewriteBasePath: 'foo' } }); - expect(error).toHaveProperty('details'); - expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']); - }); - }); - }); -}); diff --git a/src/legacy/server/core/index.ts b/src/legacy/server/core/index.ts deleted file mode 100644 index 2bdd9f26b2c22..0000000000000 --- a/src/legacy/server/core/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; -import KbnServer from '../kbn_server'; - -/** - * Exposes `kbnServer.newPlatform` through Hapi API. - * @param kbnServer KbnServer singleton instance. - * @param server Hapi server instance to expose `core` on. - */ -export function coreMixin(kbnServer: KbnServer, server: Server) { - // we suppress type error because hapi expect a function here not an object - server.decorate('server', 'newPlatform', kbnServer.newPlatform as any); -} diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js deleted file mode 100644 index 0fb51b341c3dd..0000000000000 --- a/src/legacy/server/http/index.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { format } from 'url'; -import Boom from '@hapi/boom'; - -export default async function (kbnServer, server) { - server = kbnServer.server; - - const getBasePath = (request) => kbnServer.newPlatform.setup.core.http.basePath.get(request); - - server.route({ - method: 'GET', - path: '/{p*}', - handler: function (req, h) { - const path = req.path; - if (path === '/' || path.charAt(path.length - 1) !== '/') { - throw Boom.notFound(); - } - const basePath = getBasePath(req); - const pathPrefix = basePath ? `${basePath}/` : ''; - return h - .redirect( - format({ - search: req.url.search, - pathname: pathPrefix + path.slice(0, -1), - }) - ) - .permanent(true); - }, - }); -} diff --git a/src/legacy/server/jest.config.js b/src/legacy/server/jest.config.js deleted file mode 100644 index 0a7322d2985fa..0000000000000 --- a/src/legacy/server/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/server'], -}; diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts deleted file mode 100644 index 3fe0f5899668f..0000000000000 --- a/src/legacy/server/kbn_server.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Server } from '@hapi/hapi'; - -import { - CoreSetup, - CoreStart, - EnvironmentMode, - LoggerFactory, - PackageInfo, - LegacyServiceSetupDeps, -} from '../../core/server'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyConfig } from '../../core/server/legacy'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { UiPlugins } from '../../core/server/plugins'; - -// lot of legacy code was assuming this type only had these two methods -export type KibanaConfig = Pick; - -// Extend the defaults with the plugins and server methods we need. -declare module 'hapi' { - interface PluginProperties { - spaces: any; - } - - interface Server { - config: () => KibanaConfig; - newPlatform: KbnServer['newPlatform']; - } -} - -type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; - -export interface PluginsSetup { - [key: string]: object; -} - -export interface KibanaCore { - __internals: { - hapiServer: LegacyServiceSetupDeps['core']['http']['server']; - rendering: LegacyServiceSetupDeps['core']['rendering']; - uiPlugins: UiPlugins; - }; - env: { - mode: Readonly; - packageInfo: Readonly; - }; - setupDeps: { - core: CoreSetup; - plugins: PluginsSetup; - }; - startDeps: { - core: CoreStart; - plugins: Record; - }; - logger: LoggerFactory; -} - -export interface NewPlatform { - __internals: KibanaCore['__internals']; - env: KibanaCore['env']; - coreContext: { - logger: KibanaCore['logger']; - }; - setup: KibanaCore['setupDeps']; - start: KibanaCore['startDeps']; - stop: null; -} - -// eslint-disable-next-line import/no-default-export -export default class KbnServer { - public readonly newPlatform: NewPlatform; - public server: Server; - public inject: Server['inject']; - - constructor(settings: Record, config: KibanaConfig, core: KibanaCore); - - public ready(): Promise; - public mixin(...fns: KbnMixinFunc[]): Promise; - public listen(): Promise; - public close(): Promise; - public applyLoggingConfiguration(settings: any): void; - public config: KibanaConfig; -} - -// Re-export commonly used hapi types. -export { Server, Request, ResponseToolkit } from '@hapi/hapi'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js deleted file mode 100644 index 4bc76b6a7706f..0000000000000 --- a/src/legacy/server/kbn_server.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { constant, once, compact, flatten } from 'lodash'; -import { reconfigureLogging } from '@kbn/legacy-logging'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { fromRoot, pkg } from '../../core/server/utils'; -import { Config } from './config'; -import httpMixin from './http'; -import { coreMixin } from './core'; -import { loggingMixin } from './logging'; - -/** - * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig - * @typedef {import('./kbn_server').KibanaCore} KibanaCore - * @typedef {import('./kbn_server').LegacyPlugins} LegacyPlugins - */ - -const rootDir = fromRoot('.'); - -export default class KbnServer { - /** - * @param {Record} settings - * @param {KibanaConfig} config - * @param {KibanaCore} core - */ - constructor(settings, config, core) { - this.name = pkg.name; - this.version = pkg.version; - this.build = pkg.build || false; - this.rootDir = rootDir; - this.settings = settings || {}; - this.config = config; - - const { setupDeps, startDeps, logger, __internals, env } = core; - - this.server = __internals.hapiServer; - this.newPlatform = { - env: { - mode: env.mode, - packageInfo: env.packageInfo, - }, - __internals, - coreContext: { - logger, - }, - setup: setupDeps, - start: startDeps, - stop: null, - }; - - this.ready = constant( - this.mixin( - // Sets global HTTP behaviors - httpMixin, - - coreMixin, - - loggingMixin - ) - ); - - this.listen = once(this.listen); - } - - /** - * Extend the KbnServer outside of the constraints of a plugin. This allows access - * to APIs that are not exposed (intentionally) to the plugins and should only - * be used when the code will be kept up to date with Kibana. - * - * @param {...function} - functions that should be called to mixin functionality. - * They are called with the arguments (kibana, server, config) - * and can return a promise to delay execution of the next mixin - * @return {Promise} - promise that is resolved when the final mixin completes. - */ - async mixin(...fns) { - for (const fn of compact(flatten(fns))) { - await fn.call(this, this, this.server, this.config); - } - } - - /** - * Tell the server to listen for incoming requests, or get - * a promise that will be resolved once the server is listening. - * - * @return undefined - */ - async listen() { - await this.ready(); - - const { server } = this; - - if (process.env.isDevCliChild) { - // help parent process know when we are ready - process.send(['SERVER_LISTENING']); - } - - return server; - } - - async close() { - if (!this.server) { - return; - } - - await this.server.stop(); - } - - async inject(opts) { - if (!this.server) { - await this.ready(); - } - - return await this.server.inject(opts); - } - - applyLoggingConfiguration(settings) { - const config = Config.withDefaultSchema(settings); - - const loggingConfig = config.get('logging'); - const opsConfig = config.get('ops'); - - reconfigureLogging(this.server, loggingConfig, opsConfig.interval); - } -} diff --git a/src/legacy/server/logging/index.js b/src/legacy/server/logging/index.js deleted file mode 100644 index 1b2ae59f4aa00..0000000000000 --- a/src/legacy/server/logging/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { setupLogging, setupLoggingRotate } from '@kbn/legacy-logging'; - -export async function loggingMixin(kbnServer, server, config) { - const loggingConfig = config.get('logging'); - const opsInterval = config.get('ops.interval'); - - await setupLogging(server, loggingConfig, opsInterval); - await setupLoggingRotate(server, loggingConfig); -} diff --git a/src/legacy/utils/artifact_type.ts b/src/legacy/utils/artifact_type.ts deleted file mode 100644 index 8243b78b15025..0000000000000 --- a/src/legacy/utils/artifact_type.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pkg } from '../../core/server/utils'; -export const IS_KIBANA_DISTRIBUTABLE = pkg.build && pkg.build.distributable === true; -export const IS_KIBANA_RELEASE = pkg.build && pkg.build.release === true; diff --git a/src/legacy/utils/deep_clone_with_buffers.test.ts b/src/legacy/utils/deep_clone_with_buffers.test.ts deleted file mode 100644 index f23e0c8496490..0000000000000 --- a/src/legacy/utils/deep_clone_with_buffers.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { deepCloneWithBuffers } from './deep_clone_with_buffers'; - -describe('deepCloneWithBuffers()', () => { - it('deep clones objects', () => { - const source = { - a: { - b: {}, - c: {}, - d: [ - { - e: 'f', - }, - ], - }, - }; - - const output = deepCloneWithBuffers(source); - - expect(source.a).toEqual(output.a); - expect(source.a).not.toBe(output.a); - - expect(source.a.b).toEqual(output.a.b); - expect(source.a.b).not.toBe(output.a.b); - - expect(source.a.c).toEqual(output.a.c); - expect(source.a.c).not.toBe(output.a.c); - - expect(source.a.d).toEqual(output.a.d); - expect(source.a.d).not.toBe(output.a.d); - - expect(source.a.d[0]).toEqual(output.a.d[0]); - expect(source.a.d[0]).not.toBe(output.a.d[0]); - }); - - it('copies buffers but keeps them buffers', () => { - const input = Buffer.from('i am a teapot', 'utf8'); - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input)).toBe(true); - expect(Buffer.isBuffer(output)).toBe(true); - expect(Buffer.compare(output, input)); - expect(output).not.toBe(input); - }); - - it('copies buffers that are deep', () => { - const input = { - a: { - b: { - c: Buffer.from('i am a teapot', 'utf8'), - }, - }, - }; - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input.a.b.c)).toBe(true); - expect(Buffer.isBuffer(output.a.b.c)).toBe(true); - expect(Buffer.compare(output.a.b.c, input.a.b.c)); - expect(output.a.b.c).not.toBe(input.a.b.c); - }); -}); diff --git a/src/legacy/utils/deep_clone_with_buffers.ts b/src/legacy/utils/deep_clone_with_buffers.ts deleted file mode 100644 index c81a572326e7c..0000000000000 --- a/src/legacy/utils/deep_clone_with_buffers.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { cloneDeepWith } from 'lodash'; - -// We should add `any` return type to overcome bug in lodash types, customizer -// in lodash 3.* can return `undefined` if cloning is handled by the lodash, but -// type of the customizer function doesn't expect that. -function cloneBuffersCustomizer(val: unknown): any { - if (Buffer.isBuffer(val)) { - return Buffer.from(val); - } -} - -export function deepCloneWithBuffers(val: T): T { - return cloneDeepWith(val, cloneBuffersCustomizer); -} diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts deleted file mode 100644 index 92fbd6ce715a4..0000000000000 --- a/src/legacy/utils/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export function unset(object: object, rawPath: string): void; diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js deleted file mode 100644 index a96caeb93aaa6..0000000000000 --- a/src/legacy/utils/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { deepCloneWithBuffers } from './deep_clone_with_buffers'; -export { unset } from './unset'; -export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; -export { IS_KIBANA_RELEASE } from './artifact_type'; diff --git a/src/legacy/utils/unset.js b/src/legacy/utils/unset.js deleted file mode 100644 index fa9a9cee77a13..0000000000000 --- a/src/legacy/utils/unset.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; - -export function unset(object, rawPath) { - if (!object) return; - const path = _.toPath(rawPath); - - switch (path.length) { - case 0: - return; - - case 1: - delete object[rawPath]; - break; - - default: - const leaf = path.pop(); - const parentPath = path.slice(); - const parent = _.get(object, parentPath); - unset(parent, leaf); - if (!_.size(parent)) { - unset(object, parentPath); - } - break; - } -} diff --git a/src/legacy/utils/unset.test.js b/src/legacy/utils/unset.test.js deleted file mode 100644 index 0c521ae046124..0000000000000 --- a/src/legacy/utils/unset.test.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { unset } from './unset'; - -describe('unset(obj, key)', function () { - describe('invalid input', function () { - it('should do nothing if not given an object', function () { - const obj = 'hello'; - unset(obj, 'e'); - expect(obj).toBe('hello'); - }); - - it('should do nothing if not given a key', function () { - const obj = { one: 1 }; - unset(obj); - expect(obj).toEqual({ one: 1 }); - }); - - it('should do nothing if given an empty string as a key', function () { - const obj = { one: 1 }; - unset(obj, ''); - expect(obj).toEqual({ one: 1 }); - }); - }); - - describe('shallow removal', function () { - let obj; - - beforeEach(function () { - obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; - }); - - it('should remove the param using a string key', function () { - unset(obj, 'two'); - expect(obj).toEqual({ one: 1, deep: { three: 3, four: 4 } }); - }); - - it('should remove the param using an array key', function () { - unset(obj, ['two']); - expect(obj).toEqual({ one: 1, deep: { three: 3, four: 4 } }); - }); - }); - - describe('deep removal', function () { - let obj; - - beforeEach(function () { - obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; - }); - - it('should remove the param using a string key', function () { - unset(obj, 'deep.three'); - expect(obj).toEqual({ one: 1, two: 2, deep: { four: 4 } }); - }); - - it('should remove the param using an array key', function () { - unset(obj, ['deep', 'three']); - expect(obj).toEqual({ one: 1, two: 2, deep: { four: 4 } }); - }); - }); - - describe('recursive removal', function () { - it('should clear object if only value is removed', function () { - const obj = { one: { two: { three: 3 } } }; - unset(obj, 'one.two.three'); - expect(obj).toEqual({}); - }); - - it('should clear object if no props are left', function () { - const obj = { one: { two: { three: 3 } } }; - unset(obj, 'one.two'); - expect(obj).toEqual({}); - }); - - it('should remove deep property, then clear the object', function () { - const obj = { one: { two: { three: 3, four: 4 } } }; - unset(obj, 'one.two.three'); - expect(obj).toEqual({ one: { two: { four: 4 } } }); - - unset(obj, 'one.two.four'); - expect(obj).toEqual({}); - }); - }); -}); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index 517a6238c2519..be5163e89367c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -1791,6 +1791,7 @@ exports[`Field for json setting should render as read only if saving is disabled maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -1897,6 +1898,7 @@ exports[`Field for json setting should render as read only with help text if ove maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -1979,6 +1981,7 @@ exports[`Field for json setting should render custom setting icon if it is custo maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2092,6 +2095,7 @@ exports[`Field for json setting should render default value if there is no user maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2181,6 +2185,7 @@ exports[`Field for json setting should render unsaved value if there are unsaved maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2305,6 +2310,7 @@ exports[`Field for json setting should render user value if there is user value maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2376,6 +2382,7 @@ exports[`Field for markdown setting should render as read only if saving is disa maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2479,6 +2486,7 @@ exports[`Field for markdown setting should render as read only with help text if maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2561,6 +2569,7 @@ exports[`Field for markdown setting should render custom setting icon if it is c maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2632,6 +2641,7 @@ exports[`Field for markdown setting should render default value if there is no u maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2721,6 +2731,7 @@ exports[`Field for markdown setting should render unsaved value if there are uns maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2838,6 +2849,7 @@ exports[`Field for markdown setting should render user value if there is user va maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index f5db5c3e371b3..d4a5020bbbb82 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -326,6 +326,7 @@ export class Field extends PureComponent {

{ coreStart = coreMock.createStart(); + capabilities = { + ...coreStart.application.capabilities, + visualize: { save: true }, + maps: { save: true }, + }; const containerOptions = { ExitFullScreenButton: () => null, @@ -83,7 +90,10 @@ beforeEach(async () => { }); test('Add to library is incompatible with Error Embeddables', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); const errorEmbeddable = new ErrorEmbeddable( 'Wow what an awful error', { id: ' 404' }, @@ -92,20 +102,37 @@ test('Add to library is incompatible with Error Embeddables', async () => { expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); }); +test('Add to library is incompatible on visualize embeddable without visualize save permissions', async () => { + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities: { ...capabilities, visualize: { save: false } }, + }); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); + test('Add to library is compatible when embeddable on dashboard has value type input', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Add to library is not compatible when embeddable input is by reference', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Add to library is not compatible when view mode is set to view', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -126,7 +153,10 @@ test('Add to library is not compatible when embeddable is not in a dashboard con mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id }, mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id }, }); - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -135,7 +165,10 @@ test('Add to library replaces embeddableId and retains panel count', async () => const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -161,7 +194,10 @@ test('Add to library returns reference type input', async () => { }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); await action.execute({ embeddable }); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index ef730e16bc5cf..fa102a9415b3f 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -18,7 +18,7 @@ import { isReferenceOrValueEmbeddable, isErrorEmbeddable, } from '../../services/embeddable'; -import { NotificationsStart } from '../../services/core'; +import { ApplicationStart, NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -33,7 +33,12 @@ export class AddToLibraryAction implements Action { public readonly id = ACTION_ADD_TO_LIBRARY; public order = 15; - constructor(private deps: { toasts: NotificationsStart['toasts'] }) {} + constructor( + private deps: { + toasts: NotificationsStart['toasts']; + capabilities: ApplicationStart['capabilities']; + } + ) {} public getDisplayName({ embeddable }: AddToLibraryActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { @@ -50,8 +55,15 @@ export class AddToLibraryAction implements Action { } public async isCompatible({ embeddable }: AddToLibraryActionContext) { + // TODO: Fix this, potentially by adding a 'canSave' function to embeddable interface + const canSave = + embeddable.type === 'map' + ? this.deps.capabilities.maps?.save + : this.deps.capabilities.visualize.save; + return Boolean( - !isErrorEmbeddable(embeddable) && + canSave && + !isErrorEmbeddable(embeddable) && embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index c82f17f2b29c4..829344504b16b 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -61,7 +61,8 @@ export class ClonePanelAction implements Action { embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && - embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE + embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE && + embeddable.getOutput().editable ); } diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index f981b135c4359..e5281a257ee13 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -84,6 +84,7 @@ export async function mountApp({ const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined; const activeSpaceId = spacesApi && (await spacesApi.activeSpace$.pipe(first()).toPromise())?.id; + let globalEmbedSettings: DashboardEmbedSettings | undefined; const dashboardServices: DashboardAppServices = { navigation, @@ -149,9 +150,6 @@ export async function mountApp({ const getDashboardEmbedSettings = ( routeParams: ParsedQuery ): DashboardEmbedSettings | undefined => { - if (!routeParams.embed) { - return undefined; - } return { forceShowTopNavMenu: Boolean(routeParams[dashboardUrlParams.showTopMenu]), forceShowQueryInput: Boolean(routeParams[dashboardUrlParams.showQueryInput]), @@ -162,11 +160,13 @@ export async function mountApp({ const renderDashboard = (routeProps: RouteComponentProps<{ id?: string }>) => { const routeParams = parse(routeProps.history.location.search); - const embedSettings = getDashboardEmbedSettings(routeParams); + if (routeParams.embed && !globalEmbedSettings) { + globalEmbedSettings = getDashboardEmbedSettings(routeParams); + } return ( redirect(routeProps, props)} /> diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index ae2d2b5f237c9..5bf730996ab4f 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -342,7 +342,7 @@ export class DashboardPlugin } public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { - const { notifications, overlays } = core; + const { notifications, overlays, application } = core; const { uiActions, data, share, presentationUtil, embeddable } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -370,7 +370,10 @@ export class DashboardPlugin } if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); + const addToLibraryAction = new AddToLibraryAction({ + toasts: notifications.toasts, + capabilities: application.capabilities, + }); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); @@ -386,8 +389,8 @@ export class DashboardPlugin overlays, embeddable.getStateTransfer(), { - canCreateNew: Boolean(core.application.capabilities.dashboard.createNew), - canEditExisting: !Boolean(core.application.capabilities.dashboard.hideWriteControls), + canCreateNew: Boolean(application.capabilities.dashboard.createNew), + canEditExisting: !Boolean(application.capabilities.dashboard.hideWriteControls), }, presentationUtil.ContextProvider ); diff --git a/src/plugins/dashboard/public/services/core.ts b/src/plugins/dashboard/public/services/core.ts index 7c19b2d75a967..75461729841e9 100644 --- a/src/plugins/dashboard/public/services/core.ts +++ b/src/plugins/dashboard/public/services/core.ts @@ -12,4 +12,5 @@ export { PluginInitializerContext, ScopedHistory, NotificationsStart, + ApplicationStart, } from '../../../../core/public'; diff --git a/src/plugins/data/common/field_formats/converters/color.test.ts b/src/plugins/data/common/field_formats/converters/color.test.ts index 9ce00db10b28d..4b7f2733f56fc 100644 --- a/src/plugins/data/common/field_formats/converters/color.test.ts +++ b/src/plugins/data/common/field_formats/converters/color.test.ts @@ -28,10 +28,10 @@ describe('Color Format', () => { expect(colorer.convert(99, HTML_CONTEXT_TYPE)).toBe('99'); expect(colorer.convert(100, HTML_CONTEXT_TYPE)).toBe( - '100' + '100' ); expect(colorer.convert(150, HTML_CONTEXT_TYPE)).toBe( - '150' + '150' ); expect(colorer.convert(151, HTML_CONTEXT_TYPE)).toBe('151'); }); @@ -74,22 +74,22 @@ describe('Color Format', () => { expect(converter('B', HTML_CONTEXT_TYPE)).toBe('B'); expect(converter('AAA', HTML_CONTEXT_TYPE)).toBe( - 'AAA' + 'AAA' ); expect(converter('AB', HTML_CONTEXT_TYPE)).toBe( - 'AB' + 'AB' ); expect(converter('a', HTML_CONTEXT_TYPE)).toBe('a'); expect(converter('B', HTML_CONTEXT_TYPE)).toBe('B'); expect(converter('AAA', HTML_CONTEXT_TYPE)).toBe( - 'AAA' + 'AAA' ); expect(converter('AB', HTML_CONTEXT_TYPE)).toBe( - 'AB' + 'AB' ); expect(converter('AB <', HTML_CONTEXT_TYPE)).toBe( - 'AB <' + 'AB <' ); expect(converter('a', HTML_CONTEXT_TYPE)).toBe('a'); }); diff --git a/src/plugins/data/common/field_formats/converters/color.ts b/src/plugins/data/common/field_formats/converters/color.tsx similarity index 79% rename from src/plugins/data/common/field_formats/converters/color.ts rename to src/plugins/data/common/field_formats/converters/color.tsx index f4603f32acc15..98f25fdf81811 100644 --- a/src/plugins/data/common/field_formats/converters/color.ts +++ b/src/plugins/data/common/field_formats/converters/color.tsx @@ -7,15 +7,15 @@ */ import { i18n } from '@kbn/i18n'; -import { findLast, cloneDeep, template, escape } from 'lodash'; +import React from 'react'; +import ReactDOM from 'react-dom/server'; +import { findLast, cloneDeep, escape } from 'lodash'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { asPrettyString } from '../utils'; import { DEFAULT_CONVERTER_COLOR } from '../constants/color_default'; -const convertTemplate = template('<%- val %>'); - export class ColorFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.COLOR; static title = i18n.translate('data.fieldFormats.color.title', { @@ -51,11 +51,18 @@ export class ColorFormat extends FieldFormat { htmlConvert: HtmlContextTypeConvert = (val) => { const color = this.findColorRuleForVal(val) as typeof DEFAULT_CONVERTER_COLOR; - if (!color) return escape(asPrettyString(val)); - let style = ''; - if (color.text) style += `color: ${color.text};`; - if (color.background) style += `background-color: ${color.background};`; - return convertTemplate({ val, style }); + const displayVal = escape(asPrettyString(val)); + if (!color) return displayVal; + + return ReactDOM.renderToStaticMarkup( + + ); }; } diff --git a/src/plugins/data/common/field_formats/converters/source.test.ts b/src/plugins/data/common/field_formats/converters/source.test.ts index f0576142892e2..655cf315a05a4 100644 --- a/src/plugins/data/common/field_formats/converters/source.test.ts +++ b/src/plugins/data/common/field_formats/converters/source.test.ts @@ -9,6 +9,7 @@ import { SourceFormat } from './source'; import { HtmlContextTypeConvert } from '../types'; import { HTML_CONTEXT_TYPE } from '../content_types'; +import { stubIndexPatternWithFields } from '../../index_patterns/index_pattern.stub'; describe('Source Format', () => { let convertHtml: Function; @@ -31,4 +32,19 @@ describe('Source Format', () => { '{"foo":"bar","number":42,"hello":"<h1>World</h1>","also":"with \\"quotes\\" or 'single quotes'"}' ); }); + + test('should render a description list if a field is passed', () => { + const hit = { + foo: 'bar', + number: 42, + hello: '

World

', + also: 'with "quotes" or \'single quotes\'', + }; + + const indexPattern = { ...stubIndexPatternWithFields, formatHit: (h: string) => h }; + + expect(convertHtml(hit, { field: 'field', indexPattern, hit })).toMatchInlineSnapshot( + `"
foo:
bar
number:
42
hello:

World

also:
with \\"quotes\\" or 'single quotes'
"` + ); + }); }); diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.tsx similarity index 62% rename from src/plugins/data/common/field_formats/converters/source.ts rename to src/plugins/data/common/field_formats/converters/source.tsx index bacfc1ab4c737..d6176b321f3f3 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.tsx @@ -6,40 +6,34 @@ * Side Public License, v 1. */ -import { template, escape, keys } from 'lodash'; +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom/server'; +import { escape, keys } from 'lodash'; import { shortenDottedString } from '../../utils'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { UI_SETTINGS } from '../../constants'; -/** - * Remove all of the whitespace between html tags - * so that inline elements don't have extra spaces. - * - * If you have inline elements (span, a, em, etc.) and any - * amount of whitespace around them in your markup, then the - * browser will push them apart. This is ugly in certain - * scenarios and is only fixed by removing the whitespace - * from the html in the first place (or ugly css hacks). - * - * @param {string} html - the html to modify - * @return {string} - modified html - */ -function noWhiteSpace(html: string) { - const TAGS_WITH_WS = />\s+<'); +interface Props { + defPairs: Array<[string, string]>; } - -const templateHtml = ` -
- <% defPairs.forEach(function (def) { %> -
<%- def[0] %>:
-
<%= def[1] %>
- <%= ' ' %> - <% }); %> -
`; -const doTemplate = template(noWhiteSpace(templateHtml)); +const TemplateComponent = ({ defPairs }: Props) => { + return ( +
+ {defPairs.map((pair, idx) => ( + +
+
{' '} + + ))} +
+ ); +}; export class SourceFormat extends FieldFormat { static id = FIELD_FORMAT_IDS._SOURCE; @@ -70,6 +64,8 @@ export class SourceFormat extends FieldFormat { pairs.push([newField, val]); }, []); - return doTemplate({ defPairs: highlightPairs.concat(sourcePairs) }); + return ReactDOM.renderToStaticMarkup( + + ); }; } diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 0e9cf6aeb1f2f..283d276a22904 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -439,10 +439,14 @@ export class AggConfig { } fieldIsTimeField() { - const indexPattern = this.getIndexPattern(); - if (!indexPattern) return false; - const timeFieldName = indexPattern.timeFieldName; - return timeFieldName && this.fieldName() === timeFieldName; + const defaultTimeField = this.getIndexPattern()?.getTimeField?.()?.name; + const defaultTimeFields = defaultTimeField ? [defaultTimeField] : []; + const allTimeFields = + this.aggConfigs.timeFields && this.aggConfigs.timeFields.length > 0 + ? this.aggConfigs.timeFields + : defaultTimeFields; + const currentFieldName = this.fieldName(); + return allTimeFields.includes(currentFieldName); } public get type() { diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 03c702aa72fb5..4d5d49754387d 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -64,6 +64,7 @@ export type IAggConfigs = AggConfigs; export class AggConfigs { public indexPattern: IndexPattern; public timeRange?: TimeRange; + public timeFields?: string[]; private readonly typesRegistry: AggTypesRegistryStart; aggs: IAggConfig[]; @@ -83,6 +84,10 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); } + setTimeFields(timeFields: string[] | undefined) { + this.timeFields = timeFields; + } + setTimeRange(timeRange: TimeRange) { this.timeRange = timeRange; diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index d02f8e1fc5af4..1db60db507f0f 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -29,6 +29,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.AVG, fn: metrics.getAvgMetricAgg }, { name: METRIC_TYPES.SUM, fn: metrics.getSumMetricAgg }, { name: METRIC_TYPES.MEDIAN, fn: metrics.getMedianMetricAgg }, + { name: METRIC_TYPES.SINGLE_PERCENTILE, fn: metrics.getSinglePercentileMetricAgg }, { name: METRIC_TYPES.MIN, fn: metrics.getMinMetricAgg }, { name: METRIC_TYPES.MAX, fn: metrics.getMaxMetricAgg }, { name: METRIC_TYPES.STD_DEV, fn: metrics.getStdDeviationMetricAgg }, @@ -90,6 +91,7 @@ export const getAggTypesFunctions = () => [ metrics.aggGeoCentroid, metrics.aggMax, metrics.aggMedian, + metrics.aggSinglePercentile, metrics.aggMin, metrics.aggMovingAvg, metrics.aggPercentileRanks, 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 bba67640890ad..3f434b0cc1c15 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -82,6 +82,7 @@ describe('Aggs service', () => { "avg", "sum", "median", + "single_percentile", "min", "max", "std_dev", @@ -128,6 +129,7 @@ describe('Aggs service', () => { "avg", "sum", "median", + "single_percentile", "min", "max", "std_dev", @@ -194,9 +196,8 @@ describe('Aggs service', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(5); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); - expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); expect(start).toHaveProperty('datatableUtilities'); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index c5543e5037fc6..86bda5019a496 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -17,7 +17,6 @@ import { getCalculateAutoTimeExpression, } from './'; import { AggsCommonSetup, AggsCommonStart } from './types'; -import { getDateMetaByDatatableColumn } from './utils/time_column_meta'; import { getDatatableColumnUtilities } from './utils/datatable_column_meta'; /** @internal */ @@ -89,12 +88,6 @@ export class AggsCommonService { return { calculateAutoTimeExpression, - getDateMetaByDatatableColumn: getDateMetaByDatatableColumn({ - calculateAutoTimeExpression, - getIndexPattern, - getConfig, - isDefaultTimezone, - }), datatableUtilities: getDatatableColumnUtilities({ getIndexPattern, createAggConfigs, diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index 61ad66d7efdc9..4a83ae38d34db 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -64,7 +64,9 @@ export interface AggParamsDateHistogram extends BaseAggParams { useNormalizedEsInterval?: boolean; scaleMetricValues?: boolean; interval?: string; + used_interval?: string; time_zone?: string; + used_time_zone?: string; drop_partials?: boolean; format?: string; min_doc_count?: number; @@ -220,6 +222,21 @@ export const getDateHistogramBucketAgg = ({ } }, }, + { + name: 'used_interval', + default: autoInterval, + shouldShow() { + return false; + }, + write: () => {}, + serialize(val, agg) { + if (!agg) return undefined; + const { useNormalizedEsInterval } = agg.params; + const interval = agg.buckets.getInterval(useNormalizedEsInterval); + return interval.expression; + }, + toExpressionAst: () => undefined, + }, { name: 'time_zone', default: undefined, @@ -232,6 +249,18 @@ export const getDateHistogramBucketAgg = ({ output.params.time_zone = tz; }, }, + { + name: 'used_timezone', + shouldShow() { + return false; + }, + write: () => {}, + serialize(val, agg) { + if (!agg) return undefined; + return inferTimeZone(agg.params, agg.getIndexPattern(), isDefaultTimezone, getConfig); + }, + toExpressionAst: () => undefined, + }, { name: 'drop_partials', default: false, diff --git a/src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap b/src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap new file mode 100644 index 0000000000000..a8d546973b185 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggTypeMetricSinglePercentileProvider class supports scripted fields 1`] = ` +Object { + "single_percentile": Object { + "percentiles": Object { + "percents": Array [ + 95, + ], + "script": Object { + "lang": undefined, + "source": "return 456", + }, + }, + }, +} +`; diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index 7038673d5d7c4..d37b74a1a28ae 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -36,6 +36,8 @@ export * from './max_fn'; export * from './max'; export * from './median_fn'; export * from './median'; +export * from './single_percentile_fn'; +export * from './single_percentile'; export * from './metric_agg_type'; export * from './metric_agg_types'; export * from './min_fn'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index d51038d8a15e8..ac2beaf574256 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -22,6 +22,7 @@ const metricAggFilter = [ '!geo_bounds', '!geo_centroid', '!filtered_metric', + '!single_percentile', ]; export const parentPipelineType = i18n.translate( diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index c0d1be4f47f9b..2564fcb7a002b 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -28,6 +28,7 @@ const metricAggFilter: string[] = [ '!geo_bounds', '!geo_centroid', '!filtered_metric', + '!single_percentile', ]; const bucketAggFilter: string[] = []; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index 3b6c9d8a0d55d..a308153b3816b 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -20,6 +20,7 @@ export enum METRIC_TYPES { GEO_BOUNDS = 'geo_bounds', GEO_CENTROID = 'geo_centroid', MEDIAN = 'median', + SINGLE_PERCENTILE = 'single_percentile', MIN = 'min', MAX = 'max', MOVING_FN = 'moving_avg', diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts new file mode 100644 index 0000000000000..c2ba6ee1a403a --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { METRIC_TYPES } from './metric_agg_types'; + +describe('AggTypeMetricSinglePercentileProvider class', () => { + let aggConfigs: IAggConfigs; + + beforeEach(() => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: 'bytes', + }; + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: METRIC_TYPES.SINGLE_PERCENTILE, + type: METRIC_TYPES.SINGLE_PERCENTILE, + schema: 'metric', + params: { + field: 'bytes', + percentile: 95, + }, + }, + ], + { + typesRegistry, + } + ); + }); + + it('requests the percentiles aggregation in the Elasticsearch query DSL', () => { + const dsl: Record = aggConfigs.toDsl(); + + expect(dsl.single_percentile.percentiles.field).toEqual('bytes'); + expect(dsl.single_percentile.percentiles.percents).toEqual([95]); + }); + + it('points to right value within multi metric for value bucket path', () => { + expect(aggConfigs.byId(METRIC_TYPES.SINGLE_PERCENTILE)!.getValueBucketPath()).toEqual( + `${METRIC_TYPES.SINGLE_PERCENTILE}.95` + ); + }); + + it('converts the response', () => { + const agg = aggConfigs.getResponseAggs()[0]; + + expect( + agg.getValue({ + [agg.id]: { + values: { + '95.0': 123, + }, + }, + }) + ).toEqual(123); + }); + + it('produces the expected expression ast', () => { + const agg = aggConfigs.getResponseAggs()[0]; + expect(agg.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "single_percentile", + ], + "percentile": Array [ + 95, + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggSinglePercentile", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('supports scripted fields', () => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: 'bytes', + scripted: true, + language: 'painless', + script: 'return 456', + }; + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: METRIC_TYPES.SINGLE_PERCENTILE, + type: METRIC_TYPES.SINGLE_PERCENTILE, + schema: 'metric', + params: { + field: 'bytes', + percentile: 95, + }, + }, + ], + { + typesRegistry, + } + ); + + expect(aggConfigs.toDsl()).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.ts new file mode 100644 index 0000000000000..4bdafcae327cd --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.ts @@ -0,0 +1,63 @@ +/* + * Copyright 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 { aggSinglePercentileFnName } from './single_percentile_fn'; +import { MetricAggType } from './metric_agg_type'; +import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../common'; +import { BaseAggParams } from '../types'; + +const singlePercentileTitle = i18n.translate('data.search.aggs.metrics.singlePercentileTitle', { + defaultMessage: 'Percentile', +}); + +export interface AggParamsSinglePercentile extends BaseAggParams { + field: string; + percentile: number; +} + +export const getSinglePercentileMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.SINGLE_PERCENTILE, + expressionName: aggSinglePercentileFnName, + dslName: 'percentiles', + title: singlePercentileTitle, + valueType: 'number', + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.metrics.singlePercentileLabel', { + defaultMessage: 'Percentile {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); + }, + getValueBucketPath(aggConfig) { + return `${aggConfig.id}.${aggConfig.params.percentile}`; + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], + }, + { + name: 'percentile', + default: 95, + write: (agg, output) => { + output.params.percents = [agg.params.percentile]; + }, + }, + ], + getValue(agg, bucket) { + let valueKey = String(agg.params.percentile); + if (Number.isInteger(agg.params.percentile)) { + valueKey += '.0'; + } + return bucket[agg.id].values[valueKey]; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts new file mode 100644 index 0000000000000..e7ef22c6faeee --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts @@ -0,0 +1,94 @@ +/* + * Copyright 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 { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggSinglePercentileFnName = 'aggSinglePercentile'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggSinglePercentileFnName, + Input, + AggArgs, + Output +>; + +export const aggSinglePercentile = (): FunctionDefinition => ({ + name: aggSinglePercentileFnName, + help: i18n.translate('data.search.aggs.function.metrics.singlePercentile.help', { + defaultMessage: 'Generates a serialized agg config for a single percentile agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.singlePercentile.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.singlePercentile.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + percentile: { + types: ['number'], + required: true, + help: i18n.translate('data.search.aggs.metrics.singlePercentile.percentile.help', { + defaultMessage: 'Percentile to fetch', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.SINGLE_PERCENTILE, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index e57410962fc08..675be2323b93e 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -9,7 +9,6 @@ import { Assign } from '@kbn/utility-types'; import { DatatableColumn } from 'src/plugins/expressions'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { TimeRange } from '../../query'; import { aggAvg, aggBucketAvg, @@ -57,6 +56,7 @@ import { AggParamsIpRange, AggParamsMax, AggParamsMedian, + AggParamsSinglePercentile, AggParamsMin, AggParamsMovingAvg, AggParamsPercentileRanks, @@ -86,6 +86,7 @@ import { METRIC_TYPES, AggConfig, aggFilteredMetric, + aggSinglePercentile, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -106,19 +107,6 @@ export interface AggsCommonSetup { /** @internal */ export interface AggsCommonStart { calculateAutoTimeExpression: ReturnType; - /** - * Helper function returning meta data about use date intervals for a data table column. - * If the column is not a column created by a date histogram aggregation of the esaggs data source, - * this function will return undefined. - * - * Otherwise, it will return the following attributes in an object: - * * `timeZone` time zone used to create the buckets (important e.g. for DST), - * * `timeRange` total time range of the fetch data (to infer partial buckets at the beginning and end of the data) - * * `interval` Interval used on elasticsearch (`auto` resolved to the actual interval) - */ - getDateMetaByDatatableColumn: ( - column: DatatableColumn - ) => Promise; datatableUtilities: { getIndexPattern: (column: DatatableColumn) => Promise; getAggConfig: (column: DatatableColumn) => Promise; @@ -183,6 +171,7 @@ export interface AggParamsMapping { [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; [METRIC_TYPES.MAX]: AggParamsMax; [METRIC_TYPES.MEDIAN]: AggParamsMedian; + [METRIC_TYPES.SINGLE_PERCENTILE]: AggParamsSinglePercentile; [METRIC_TYPES.MIN]: AggParamsMin; [METRIC_TYPES.STD_DEV]: AggParamsStdDeviation; [METRIC_TYPES.SUM]: AggParamsSum; @@ -229,6 +218,7 @@ export interface AggFunctionsMapping { aggGeoCentroid: ReturnType; aggMax: ReturnType; aggMedian: ReturnType; + aggSinglePercentile: ReturnType; aggMin: ReturnType; aggMovingAvg: ReturnType; aggPercentileRanks: ReturnType; diff --git a/src/plugins/data/common/search/aggs/utils/get_date_histogram_meta.ts b/src/plugins/data/common/search/aggs/utils/get_date_histogram_meta.ts new file mode 100644 index 0000000000000..0a3aab6286d89 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_date_histogram_meta.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import { TimeRange } from '../../../types'; +import type { AggParamsDateHistogram } from '../buckets'; +import { BUCKET_TYPES } from '../buckets/bucket_agg_types'; + +/** + * Helper function returning the used interval, used time zone and applied time filters for data table column created by the date_histogramm agg type. + * "auto" will get expanded to the actually used interval. + * If the column is not a column created by a date_histogram aggregation of the esaggs data source, + * this function will return undefined. + */ +export const getDateHistogramMetaDataByDatatableColumn = (column: DatatableColumn) => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; + const params = (column.meta.sourceParams.params as unknown) as AggParamsDateHistogram; + + let interval: string | undefined; + if (params.used_interval && params.used_interval !== 'auto') { + interval = params.used_interval; + } + + return { + interval, + timeZone: params.used_time_zone, + timeRange: column.meta.sourceParams.appliedTimeRange as TimeRange | undefined, + }; +}; diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index f90e8f88546f4..c92653e843233 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -8,6 +8,7 @@ export * from './calculate_auto_time_expression'; export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histogram_interval'; +export { getDateHistogramMetaDataByDatatableColumn } from './get_date_histogram_meta'; export * from './date_interval_utils'; export * from './get_format_with_aggs'; export * from './ipv4_address'; diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts deleted file mode 100644 index d9527ecd92a12..0000000000000 --- a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { BUCKET_TYPES } from '../buckets'; -import { DateMetaByColumnDeps, getDateMetaByDatatableColumn } from './time_column_meta'; - -describe('getDateMetaByDatatableColumn', () => { - let params: DateMetaByColumnDeps; - beforeEach(() => { - params = { - calculateAutoTimeExpression: jest.fn().mockReturnValue('5m'), - getIndexPattern: jest.fn().mockResolvedValue({}), - isDefaultTimezone: jest.fn().mockReturnValue(true), - getConfig: jest.fn(), - }; - }); - - it('returns nothing on column from other data source', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'essql', - }, - }) - ).toEqual(undefined); - }); - - it('returns nothing on non date histogram column', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.TERMS, - }, - }, - }) - ).toEqual(undefined); - }); - - it('returns time range, time zone and interval', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - interval: '1h', - }, - appliedTimeRange: { - from: 'now-5d', - to: 'now', - }, - }, - }, - }) - ).toEqual({ - timeZone: 'UTC', - timeRange: { - from: 'now-5d', - to: 'now', - }, - interval: '1h', - }); - }); - - it('throws if unable to resolve interval', async () => { - await expect( - getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - interval: 'auto', - }, - }, - }, - }) - ).rejects.toBeDefined(); - - await expect( - getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - }, - }, - }, - }) - ).rejects.toBeDefined(); - }); - - it('returns resolved auto interval', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - interval: 'auto', - }, - appliedTimeRange: { - from: '2020-10-05T00:00:00.000Z', - to: '2020-10-10T00:00:00.000Z', - }, - }, - }, - }) - ).toEqual( - expect.objectContaining({ - interval: '5m', - }) - ); - }); -}); diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts deleted file mode 100644 index b0912803908cc..0000000000000 --- a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DatatableColumn } from 'src/plugins/expressions/common'; -import { IndexPattern } from '../../../index_patterns'; - -import { TimeRange } from '../../../types'; -import { AggParamsDateHistogram, BUCKET_TYPES } from '../buckets'; -import { inferTimeZone } from './infer_time_zone'; - -export interface DateMetaByColumnDeps { - calculateAutoTimeExpression: (range: TimeRange) => string | undefined; - getIndexPattern: (id: string) => Promise; - isDefaultTimezone: () => boolean; - getConfig: (key: string) => T; -} - -export const getDateMetaByDatatableColumn = ({ - calculateAutoTimeExpression, - getIndexPattern, - isDefaultTimezone, - getConfig, -}: DateMetaByColumnDeps) => async ( - column: DatatableColumn -): Promise => { - if (column.meta.source !== 'esaggs') return; - if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; - const params = column.meta.sourceParams.params as AggParamsDateHistogram; - const appliedTimeRange = column.meta.sourceParams.appliedTimeRange as TimeRange | undefined; - - const tz = inferTimeZone( - params, - await getIndexPattern(column.meta.sourceParams.indexPatternId as string), - isDefaultTimezone, - getConfig - ); - - const interval = - params.interval === 'auto' && appliedTimeRange - ? calculateAutoTimeExpression(appliedTimeRange) - : params.interval; - - if (!interval || interval === 'auto') { - throw new Error('time interval could not be determined'); - } - - return { - timeZone: tz, - timeRange: appliedTimeRange, - interval, - }; -}; diff --git a/src/plugins/data/public/search/expressions/__snapshots__/es_raw_response.test.ts.snap b/src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap similarity index 100% rename from src/plugins/data/public/search/expressions/__snapshots__/es_raw_response.test.ts.snap rename to src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap diff --git a/src/plugins/data/public/search/expressions/es_raw_response.test.ts b/src/plugins/data/common/search/expressions/es_raw_response.test.ts similarity index 100% rename from src/plugins/data/public/search/expressions/es_raw_response.test.ts rename to src/plugins/data/common/search/expressions/es_raw_response.test.ts diff --git a/src/plugins/data/public/search/expressions/es_raw_response.ts b/src/plugins/data/common/search/expressions/es_raw_response.ts similarity index 100% rename from src/plugins/data/public/search/expressions/es_raw_response.ts rename to src/plugins/data/common/search/expressions/es_raw_response.ts diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index f8604266cd6e0..7580032b0dd85 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -33,6 +33,7 @@ describe('esaggs expression function - public', () => { setTimeRange: jest.fn(), toDsl: jest.fn().mockReturnValue({ aggs: {} }), onSearchRequestStart: jest.fn(), + setTimeFields: jest.fn(), } as unknown) as jest.Mocked, filters: undefined, indexPattern: ({ id: 'logstash-*' } as unknown) as jest.Mocked, diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index e2ee1a31757cb..72d9cc4095570 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -73,6 +73,7 @@ export const handleRequest = async ({ const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); aggs.setTimeRange(timeRange as TimeRange); + aggs.setTimeFields(timeFields); // For now we need to mirror the history of the passed search source, since // the request inspector wouldn't work otherwise. diff --git a/src/plugins/data/common/search/expressions/esdsl.ts b/src/plugins/data/common/search/expressions/esdsl.ts new file mode 100644 index 0000000000000..dee1b19eb3360 --- /dev/null +++ b/src/plugins/data/common/search/expressions/esdsl.ts @@ -0,0 +1,194 @@ +/* + * Copyright 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 { EsRawResponse } from './es_raw_response'; +import { RequestStatistics, RequestAdapter } from '../../../../inspector/common'; +import { ISearchGeneric, KibanaContext } from '..'; +import { buildEsQuery, getEsQueryConfig } from '../../es_query/es_query'; +import { UiSettingsCommon } from '../../index_patterns'; + +const name = 'esdsl'; + +type Input = KibanaContext | null; +type Output = Promise; + +interface Arguments { + dsl: string; + index: string; + size: number; +} + +export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof name, + Input, + Arguments, + Output +>; + +/** @internal */ +export interface EsdslStartDependencies { + search: ISearchGeneric; + uiSettingsClient: UiSettingsCommon; +} + +export const getEsdslFn = ({ + getStartDependencies, +}: { + getStartDependencies: (getKibanaRequest: any) => Promise; +}) => { + const esdsl: EsdslExpressionFunctionDefinition = { + name, + type: 'es_raw_response', + inputTypes: ['kibana_context', 'null'], + help: i18n.translate('data.search.esdsl.help', { + defaultMessage: 'Run Elasticsearch request', + }), + args: { + dsl: { + types: ['string'], + aliases: ['_', 'q', 'query'], + help: i18n.translate('data.search.esdsl.q.help', { + defaultMessage: 'Query DSL', + }), + required: true, + }, + index: { + types: ['string'], + help: i18n.translate('data.search.esdsl.index.help', { + defaultMessage: 'ElasticSearch index to query', + }), + required: true, + }, + size: { + types: ['number'], + help: i18n.translate('data.search.esdsl.size.help', { + defaultMessage: 'ElasticSearch searchAPI size parameter', + }), + default: 10, + }, + }, + async fn(input, args, { inspectorAdapters, abortSignal, getKibanaRequest }) { + const { search, uiSettingsClient } = await getStartDependencies(getKibanaRequest); + + const dsl = JSON.parse(args.dsl); + + if (input) { + const esQueryConfigs = getEsQueryConfig(uiSettingsClient as any); + const query = buildEsQuery( + undefined, // args.index, + input.query || [], + input.filters || [], + esQueryConfigs + ); + + if (dsl.query) { + query.bool.must.push(dsl.query); + } + + dsl.query = query; + } + + if (!inspectorAdapters.requests) { + inspectorAdapters.requests = new RequestAdapter(); + } + + const request = inspectorAdapters.requests.start( + i18n.translate('data.search.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.search.es_search.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + } + ); + + request.stats({ + indexPattern: { + label: i18n.translate('data.search.es_search.indexPatternLabel', { + defaultMessage: 'Index pattern', + }), + value: args.index, + description: i18n.translate('data.search.es_search.indexPatternDescription', { + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', + }), + }, + }); + + try { + const { rawResponse } = await search( + { + params: { + index: args.index, + size: args.size, + body: dsl, + }, + }, + { abortSignal } + ).toPromise(); + + const stats: RequestStatistics = {}; + + if (rawResponse?.took) { + stats.queryTime = { + label: i18n.translate('data.search.es_search.queryTimeLabel', { + defaultMessage: 'Query time', + }), + value: i18n.translate('data.search.es_search.queryTimeValue', { + defaultMessage: '{queryTime}ms', + values: { queryTime: rawResponse.took }, + }), + description: i18n.translate('data.search.es_search.queryTimeDescription', { + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', + }), + }; + } + + if (rawResponse?.hits) { + stats.hitsTotal = { + label: i18n.translate('data.search.es_search.hitsTotalLabel', { + defaultMessage: 'Hits (total)', + }), + value: `${rawResponse.hits.total}`, + description: i18n.translate('data.search.es_search.hitsTotalDescription', { + defaultMessage: 'The number of documents that match the query.', + }), + }; + + stats.hits = { + label: i18n.translate('data.search.es_search.hitsLabel', { + defaultMessage: 'Hits', + }), + value: `${rawResponse.hits.hits.length}`, + description: i18n.translate('data.search.es_search.hitsDescription', { + defaultMessage: 'The number of documents returned by the query.', + }), + }; + } + + request.stats(stats).ok({ json: rawResponse }); + request.json(dsl); + + return { + type: 'es_raw_response', + body: rawResponse, + }; + } catch (e) { + request.error({ json: e }); + throw e; + } + }, + }; + return esdsl; +}; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index b80cbad778a11..6df6dacddeb2d 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -23,3 +23,5 @@ export * from './range_filter'; export * from './kibana_filter'; export * from './filters_to_ast'; export * from './timerange'; +export * from './es_raw_response'; +export * from './esdsl'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 1838ca43e8c23..c47cd6cd9740d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -313,6 +313,7 @@ import { toAbsoluteDates, boundsDescendingRaw, getNumberHistogramIntervalByDatatableColumn, + getDateHistogramMetaDataByDatatableColumn, // expressions utils getRequestInspectorStats, getResponseInspectorStats, @@ -423,6 +424,7 @@ export const search = { toAbsoluteDates, boundsDescendingRaw, getNumberHistogramIntervalByDatatableColumn, + getDateHistogramMetaDataByDatatableColumn, }, getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 746d035e9bfb6..7f243cefd08b6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -40,7 +40,6 @@ import { EventEmitter } from 'events'; import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { ExpressionFunctionDefinition as ExpressionFunctionDefinition_2 } from 'src/plugins/expressions/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; @@ -132,7 +131,7 @@ export class AggConfig { enabled: boolean; static ensureIds(list: any[]): any[]; // (undocumented) - fieldIsTimeField(): boolean | "" | undefined; + fieldIsTimeField(): boolean; // (undocumented) fieldName(): any; // (undocumented) @@ -260,8 +259,12 @@ export class AggConfigs { // (undocumented) onSearchRequestStart(searchSource: ISearchSource_2, options?: ISearchOptions_2): Promise<[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]>; // (undocumented) + setTimeFields(timeFields: string[] | undefined): void; + // (undocumented) setTimeRange(timeRange: TimeRange): void; // (undocumented) + timeFields?: string[]; + // (undocumented) timeRange?: TimeRange; // (undocumented) toDsl(hierarchical?: boolean): Record; @@ -396,6 +399,10 @@ export interface AggFunctionsMapping { // // (undocumented) aggSignificantTerms: ReturnType; + // Warning: (ae-forgotten-export) The symbol "aggSinglePercentile" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggSinglePercentile: ReturnType; // Warning: (ae-forgotten-export) The symbol "aggStdDeviation" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -714,7 +721,7 @@ export const ES_SEARCH_STRATEGY = "es"; // Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>; +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_36, Arguments_21, Output_36>; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts @@ -723,7 +730,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e // Warning: (ae-missing-release-tag) "EsdslExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2; +export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1857,6 +1864,8 @@ export enum METRIC_TYPES { // (undocumented) SERIAL_DIFF = "serial_diff", // (undocumented) + SINGLE_PERCENTILE = "single_percentile", + // (undocumented) STD_DEV = "std_dev", // (undocumented) SUM = "sum", @@ -2240,7 +2249,7 @@ export const search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; @@ -2273,6 +2282,11 @@ export const search: { intervalLabel: string; })[]; getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; + getDateHistogramMetaDataByDatatableColumn: (column: import("../../expressions").DatatableColumn) => { + interval: string | undefined; + timeZone: string | undefined; + timeRange: import("../common").TimeRange | undefined; + } | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; @@ -2642,7 +2656,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/aggs/types.ts:141:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:129:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts @@ -2676,21 +2690,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts 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 7e9170b98f132..cd2ee69d33996 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(11); - expect(start.types.getAll().metrics.length).toBe(22); + expect(start.types.getAll().metrics.length).toBe(23); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(12); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(23); + expect(start.types.getAll().metrics.length).toBe(24); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); @@ -79,9 +79,8 @@ describe('AggsService - public', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(5); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); - expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); expect(start).toHaveProperty('datatableUtilities'); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 26f19ca7eaef4..f603bd733f601 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -91,16 +91,13 @@ export class AggsService { public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { const isDefaultTimezone = () => uiSettings.isDefault('dateFormat:tz'); - const { - calculateAutoTimeExpression, - getDateMetaByDatatableColumn, - datatableUtilities, - types, - } = this.aggsCommonService.start({ - getConfig: this.getConfig!, - getIndexPattern: indexPatterns.get, - isDefaultTimezone, - }); + const { calculateAutoTimeExpression, datatableUtilities, types } = this.aggsCommonService.start( + { + getConfig: this.getConfig!, + getIndexPattern: indexPatterns.get, + isDefaultTimezone, + } + ); const aggTypesDependencies: AggTypesDependencies = { calculateBounds: this.calculateBounds, @@ -140,7 +137,6 @@ export class AggsService { return { calculateAutoTimeExpression, - getDateMetaByDatatableColumn, datatableUtilities, createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index 9c90af801bdac..fb50058f08348 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -56,7 +56,6 @@ export const searchAggsSetupMock = (): AggsSetup => ({ export const searchAggsStartMock = (): AggsStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), - getDateMetaByDatatableColumn: jest.fn(), datatableUtilities: { isFilterable: jest.fn(), getAggConfig: jest.fn(), diff --git a/src/plugins/data/public/search/expressions/esdsl.test.ts b/src/plugins/data/public/search/expressions/esdsl.test.ts index edaaa0cae2b7a..197957442cf30 100644 --- a/src/plugins/data/public/search/expressions/esdsl.test.ts +++ b/src/plugins/data/public/search/expressions/esdsl.test.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { esdsl } from './esdsl'; +import { getEsdsl } from './esdsl'; +import { MockedKeys } from '@kbn/utility-types/target/jest'; +import { EsdslExpressionFunctionDefinition } from '../../../common/search/expressions'; +import { StartServicesAccessor } from 'kibana/public'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +import { of } from 'rxjs'; jest.mock('@kbn/i18n', () => { return { @@ -16,26 +21,38 @@ jest.mock('@kbn/i18n', () => { }; }); -jest.mock('../../services', () => ({ - getUiSettings: () => ({ - get: () => true, - }), - getSearchService: () => ({ - search: jest.fn((params: any) => { - return { - toPromise: async () => { - return { rawResponse: params }; +describe('esdsl', () => { + let getStartServices: StartServicesAccessor; + let startDependencies: MockedKeys< + StartServicesAccessor + >; + let esdsl: EsdslExpressionFunctionDefinition; + + beforeEach(() => { + jest.clearAllMocks(); + startDependencies = [ + { + uiSettings: { + get: jest.fn().mockReturnValue(true), }, - }; - }), - }), -})); + }, + {}, + { + search: { + search: jest.fn((params: any) => of({ rawResponse: params })), + }, + }, + ]; + getStartServices = jest + .fn() + .mockResolvedValue(new Promise((resolve) => resolve(startDependencies))); + esdsl = getEsdsl({ getStartServices }); + }); -describe('esdsl', () => { describe('correctly handles input', () => { test('throws on invalid json input', async () => { const fn = async function () { - await esdsl().fn(null, { dsl: 'invalid json', index: 'test', size: 0 }, { + await esdsl.fn(null, { dsl: 'invalid json', index: 'test', size: 0 }, { inspectorAdapters: {}, } as any); }; @@ -50,7 +67,7 @@ describe('esdsl', () => { }); test('adds filters', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', filters: [ @@ -68,7 +85,7 @@ describe('esdsl', () => { }); test('adds filters to query with filters', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', filters: [ @@ -90,7 +107,7 @@ describe('esdsl', () => { }); test('adds query', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', query: { language: 'lucene', query: '*' }, @@ -103,7 +120,7 @@ describe('esdsl', () => { }); test('adds query to a query with filters', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', query: { language: 'lucene', query: '*' }, @@ -120,7 +137,7 @@ describe('esdsl', () => { }); test('ignores timerange', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', timeRange: { from: 'now-15m', to: 'now' }, @@ -134,7 +151,7 @@ describe('esdsl', () => { }); test('correctly handles filter, query and timerange on context', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', query: { language: 'lucene', query: '*' }, diff --git a/src/plugins/data/public/search/expressions/esdsl.ts b/src/plugins/data/public/search/expressions/esdsl.ts index 290f488ef29b5..1dda44ee8993e 100644 --- a/src/plugins/data/public/search/expressions/esdsl.ts +++ b/src/plugins/data/public/search/expressions/esdsl.ts @@ -6,182 +6,37 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; - -import { getSearchService, getUiSettings } from '../../services'; -import { EsRawResponse } from './es_raw_response'; -import { RequestStatistics, RequestAdapter } from '../../../../inspector/common'; -import { IEsSearchResponse, KibanaContext } from '../../../common/search'; -import { buildEsQuery, getEsQueryConfig } from '../../../common/es_query/es_query'; -import { DataPublicPluginStart } from '../../types'; - -const name = 'esdsl'; - -type Input = KibanaContext | null; -type Output = Promise; - -interface Arguments { - dsl: string; - index: string; - size: number; -} - -export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition< - typeof name, - Input, - Arguments, - Output ->; - -export const esdsl = (): EsdslExpressionFunctionDefinition => ({ - name, - type: 'es_raw_response', - inputTypes: ['kibana_context', 'null'], - help: i18n.translate('data.search.esdsl.help', { - defaultMessage: 'Run Elasticsearch request', - }), - args: { - dsl: { - types: ['string'], - aliases: ['_', 'q', 'query'], - help: i18n.translate('data.search.esdsl.q.help', { - defaultMessage: 'Query DSL', - }), - required: true, - }, - index: { - types: ['string'], - help: i18n.translate('data.search.esdsl.index.help', { - defaultMessage: 'ElasticSearch index to query', - }), - required: true, - }, - size: { - types: ['number'], - help: i18n.translate('data.search.esdsl.size.help', { - defaultMessage: 'ElasticSearch searchAPI size parameter', - }), - default: 10, - }, - }, - async fn(input, args, { inspectorAdapters, abortSignal }) { - const searchService: DataPublicPluginStart['search'] = getSearchService(); - - const dsl = JSON.parse(args.dsl); - - if (input) { - const esQueryConfigs = getEsQueryConfig(getUiSettings()); - const query = buildEsQuery( - undefined, // args.index, - input.query || [], - input.filters || [], - esQueryConfigs - ); - - if (!dsl.query) { - dsl.query = query; - } else { - query.bool.must.push(dsl.query); - dsl.query = query; - } - } - - if (!inspectorAdapters.requests) { - inspectorAdapters.requests = new RequestAdapter(); - } - - const request = inspectorAdapters.requests.start( - i18n.translate('data.search.dataRequest.title', { - defaultMessage: 'Data', - }), - { - description: i18n.translate('data.search.es_search.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), - } - ); - - request.stats({ - indexPattern: { - label: i18n.translate('data.search.es_search.indexPatternLabel', { - defaultMessage: 'Index pattern', - }), - value: args.index, - description: i18n.translate('data.search.es_search.indexPatternDescription', { - defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', - }), - }, - }); - - let res: IEsSearchResponse; - try { - res = await searchService - .search( - { - params: { - index: args.index, - size: args.size, - body: dsl, - }, - }, - { abortSignal } - ) - .toPromise(); - - const stats: RequestStatistics = {}; - const resp = res.rawResponse; - - if (resp && resp.took) { - stats.queryTime = { - label: i18n.translate('data.search.es_search.queryTimeLabel', { - defaultMessage: 'Query time', - }), - value: i18n.translate('data.search.es_search.queryTimeValue', { - defaultMessage: '{queryTime}ms', - values: { queryTime: resp.took }, - }), - description: i18n.translate('data.search.es_search.queryTimeDescription', { - defaultMessage: - 'The time it took to process the query. ' + - 'Does not include the time to send the request or parse it in the browser.', - }), - }; - } - - if (resp && resp.hits) { - stats.hitsTotal = { - label: i18n.translate('data.search.es_search.hitsTotalLabel', { - defaultMessage: 'Hits (total)', - }), - value: `${resp.hits.total}`, - description: i18n.translate('data.search.es_search.hitsTotalDescription', { - defaultMessage: 'The number of documents that match the query.', - }), - }; - - stats.hits = { - label: i18n.translate('data.search.es_search.hitsLabel', { - defaultMessage: 'Hits', - }), - value: `${resp.hits.hits.length}`, - description: i18n.translate('data.search.es_search.hitsDescription', { - defaultMessage: 'The number of documents returned by the query.', - }), - }; - } - - request.stats(stats).ok({ json: resp }); - request.json(dsl); - +import { StartServicesAccessor } from 'src/core/public'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +import { getEsdslFn } from '../../../common/search/expressions/esdsl'; +import { UiSettingsCommon } from '../../../common/index_patterns'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getEsdsl({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getEsdslFn({ + getStartDependencies: async () => { + const [core, , { search }] = await getStartServices(); return { - type: 'es_raw_response', - body: resp, + uiSettingsClient: (core.uiSettings as any) as UiSettingsCommon, + search: search.search, }; - } catch (e) { - request.error({ json: e }); - throw e; - } - }, -}); + }, + }); +} diff --git a/src/plugins/data/public/search/expressions/index.ts b/src/plugins/data/public/search/expressions/index.ts index cb4ca4b432610..d60ab610d27b5 100644 --- a/src/plugins/data/public/search/expressions/index.ts +++ b/src/plugins/data/public/search/expressions/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export * from './es_raw_response'; export * from './esaggs'; export * from './esdsl'; +export * from '../../../common/search/expressions'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index a3acd775ee892..83a44b6f68af6 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -33,6 +33,7 @@ import { rangeFilterFunction, kibanaFilterFunction, phraseFilterFunction, + esRawResponse, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -40,7 +41,7 @@ import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -import { esdsl, esRawResponse, getEsaggs } from './expressions'; +import { getEsaggs, getEsdsl } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session'; import { ConfigSchema } from '../../config'; @@ -126,7 +127,11 @@ export class SearchService implements Plugin { expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); - expressions.registerFunction(esdsl); + expressions.registerFunction( + getEsdsl({ getStartServices } as { + getStartServices: StartServicesAccessor; + }) + ); expressions.registerType(esRawResponse); const aggs = this.aggsService.setup({ diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 5639229e1ff31..d2f04228ed396 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -85,7 +85,7 @@ class FilterEditorUI extends Component { public render() { return (
- + { panelPaddingSize="none" repositionOnScroll > - +
- +

{ const newQueryString = value.substr(0, start) + text + value.substr(end); this.reportUiCounter?.( - METRIC_TYPE.LOADED, - `query_string:${type}:suggestions_select_position`, - listIndex + METRIC_TYPE.CLICK, + `query_string:${type}:suggestions_select_position_${listIndex}` ); this.reportUiCounter?.( - METRIC_TYPE.LOADED, - `query_string:${type}:suggestions_select_q_length`, - end - start + METRIC_TYPE.CLICK, + `query_string:${type}:suggestions_select_q_length_${end - start}` ); this.onQueryStringChange(newQueryString); diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx index 34e6c2c3452e6..83e7c0a9cf4fb 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx @@ -202,7 +202,7 @@ export function SavedQueryManagementComponent({ className="kbnSavedQueryManagement__popover" data-test-subj="saved-query-management-popover" > - + {savedQueryPopoverTitleText} {savedQueries.length > 0 ? ( @@ -234,7 +234,7 @@ export function SavedQueryManagementComponent({ )} - + async ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient +) => { + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + return new IndexPatternsCommonService({ + uiSettings: new UiSettingsServerToCommon(uiSettingsClient), + savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), + apiClient: new IndexPatternsApiServer(elasticsearchClient), + fieldFormats: formats, + onError: (error) => { + logger.error(error); + }, + onNotification: ({ title, text }) => { + logger.warn(`${title} : ${text}`); + }, + }); +}; + export class IndexPatternsServiceProvider implements Plugin { public setup( - core: CoreSetup, - { expressions }: IndexPatternsServiceSetupDeps + core: CoreSetup, + { expressions, usageCollection }: IndexPatternsServiceSetupDeps ) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); @@ -53,32 +87,18 @@ export class IndexPatternsServiceProvider implements Plugin { - const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); - const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); - - return new IndexPatternsCommonService({ - uiSettings: new UiSettingsServerToCommon(uiSettingsClient), - savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient), - fieldFormats: formats, - onError: (error) => { - logger.error(error); - }, - onNotification: ({ title, text }) => { - logger.warn(`${title} : ${text}`); - }, - }); - }, + indexPatternsServiceFactory: indexPatternsServiceFactory({ + logger, + uiSettings, + fieldFormats, + }), }; } } diff --git a/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.test.ts b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.test.ts new file mode 100644 index 0000000000000..c43431e10731a --- /dev/null +++ b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + minMaxAvgLoC, + updateMin, + updateMax, + getIndexPatternTelemetry, +} from './register_index_pattern_usage_collection'; +import { IndexPatternsCommonService } from '..'; + +const scriptA = 'emit(0);'; +const scriptB = 'emit(1);\nemit(2);'; +const scriptC = 'emit(3);\nemit(4)\nemit(5)'; + +const scriptedFieldA = { script: scriptA }; +const scriptedFieldB = { script: scriptB }; +const scriptedFieldC = { script: scriptC }; + +const runtimeFieldA = { runtimeField: { script: { source: scriptA } } }; +const runtimeFieldB = { runtimeField: { script: { source: scriptB } } }; +const runtimeFieldC = { runtimeField: { script: { source: scriptC } } }; + +const indexPatterns = ({ + getIds: async () => [1, 2, 3], + get: jest.fn().mockResolvedValue({ + getScriptedFields: () => [], + fields: [], + }), +} as any) as IndexPatternsCommonService; + +describe('index pattern usage collection', () => { + it('minMaxAvgLoC calculates min, max, and average ', () => { + const scripts = [scriptA, scriptB, scriptC]; + expect(minMaxAvgLoC(scripts)).toEqual({ min: 1, max: 3, avg: 2 }); + expect(minMaxAvgLoC([undefined, undefined, undefined])).toEqual({ min: 0, max: 0, avg: 0 }); + }); + + it('updateMin returns minimum value', () => { + expect(updateMin(undefined, 1)).toEqual(1); + expect(updateMin(1, 0)).toEqual(0); + }); + + it('updateMax returns maximum value', () => { + expect(updateMax(undefined, 1)).toEqual(1); + expect(updateMax(1, 0)).toEqual(1); + }); + + describe('calculates index pattern usage', () => { + const countSummaryDefault = { + min: undefined, + max: undefined, + avg: undefined, + }; + + it('when there are no runtime fields or scripted fields', async () => { + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 0, + indexPatternsWithRuntimeFieldCount: 0, + scriptedFieldCount: 0, + runtimeFieldCount: 0, + perIndexPattern: { + scriptedFieldCount: countSummaryDefault, + runtimeFieldCount: countSummaryDefault, + scriptedFieldLineCount: countSummaryDefault, + runtimeFieldLineCount: countSummaryDefault, + }, + }); + }); + + it('when there are both runtime fields or scripted fields', async () => { + indexPatterns.get = jest.fn().mockResolvedValue({ + getScriptedFields: () => [scriptedFieldA, scriptedFieldB, scriptedFieldC], + fields: [runtimeFieldA, runtimeFieldB, runtimeFieldC], + }); + + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 3, + indexPatternsWithRuntimeFieldCount: 3, + scriptedFieldCount: 9, + runtimeFieldCount: 9, + perIndexPattern: { + scriptedFieldCount: { min: 3, max: 3, avg: 3 }, + runtimeFieldCount: { min: 3, max: 3, avg: 3 }, + scriptedFieldLineCount: { min: 1, max: 3, avg: 2 }, + runtimeFieldLineCount: { min: 1, max: 3, avg: 2 }, + }, + }); + }); + + it('when there are only runtime fields', async () => { + indexPatterns.get = jest.fn().mockResolvedValue({ + getScriptedFields: () => [], + fields: [runtimeFieldA, runtimeFieldB, runtimeFieldC], + }); + + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 0, + indexPatternsWithRuntimeFieldCount: 3, + scriptedFieldCount: 0, + runtimeFieldCount: 9, + perIndexPattern: { + scriptedFieldCount: countSummaryDefault, + runtimeFieldCount: { min: 3, max: 3, avg: 3 }, + scriptedFieldLineCount: countSummaryDefault, + runtimeFieldLineCount: { min: 1, max: 3, avg: 2 }, + }, + }); + }); + + it('when there are only scripted fields', async () => { + indexPatterns.get = jest.fn().mockResolvedValue({ + getScriptedFields: () => [scriptedFieldA, scriptedFieldB, scriptedFieldC], + fields: [], + }); + + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 3, + indexPatternsWithRuntimeFieldCount: 0, + scriptedFieldCount: 9, + runtimeFieldCount: 0, + perIndexPattern: { + scriptedFieldCount: { min: 3, max: 3, avg: 3 }, + runtimeFieldCount: countSummaryDefault, + scriptedFieldLineCount: { min: 1, max: 3, avg: 2 }, + runtimeFieldLineCount: countSummaryDefault, + }, + }); + }); + }); +}); diff --git a/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.ts b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.ts new file mode 100644 index 0000000000000..36c2a59ce2753 --- /dev/null +++ b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.ts @@ -0,0 +1,193 @@ +/* + * Copyright 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { StartServicesAccessor } from 'src/core/server'; +import { IndexPatternsCommonService } from '..'; +import { SavedObjectsClient } from '../../../../core/server'; +import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; + +interface CountSummary { + min?: number; + max?: number; + avg?: number; +} + +interface IndexPatternUsage { + indexPatternsCount: number; + indexPatternsWithScriptedFieldCount: number; + indexPatternsWithRuntimeFieldCount: number; + scriptedFieldCount: number; + runtimeFieldCount: number; + perIndexPattern: { + scriptedFieldCount: CountSummary; + runtimeFieldCount: CountSummary; + scriptedFieldLineCount: CountSummary; + runtimeFieldLineCount: CountSummary; + }; +} + +export const minMaxAvgLoC = (scripts: Array) => { + const lengths = scripts.map((script) => script?.split(/\r\n|\r|\n/).length || 0).sort(); + return { + min: lengths[0], + max: lengths[lengths.length - 1], + avg: lengths.reduce((col, count) => col + count, 0) / lengths.length, + }; +}; + +export const updateMin = (currentMin: number | undefined, newVal: number): number => { + if (currentMin === undefined || currentMin > newVal) { + return newVal; + } else { + return currentMin; + } +}; + +export const updateMax = (currentMax: number | undefined, newVal: number): number => { + if (currentMax === undefined || currentMax < newVal) { + return newVal; + } else { + return currentMax; + } +}; + +export async function getIndexPatternTelemetry(indexPatterns: IndexPatternsCommonService) { + const ids = await indexPatterns.getIds(); + + const countSummaryDefaults: CountSummary = { + min: undefined, + max: undefined, + avg: undefined, + }; + + const results = { + indexPatternsCount: ids.length, + indexPatternsWithScriptedFieldCount: 0, + indexPatternsWithRuntimeFieldCount: 0, + scriptedFieldCount: 0, + runtimeFieldCount: 0, + perIndexPattern: { + scriptedFieldCount: { ...countSummaryDefaults }, + runtimeFieldCount: { ...countSummaryDefaults }, + scriptedFieldLineCount: { ...countSummaryDefaults }, + runtimeFieldLineCount: { ...countSummaryDefaults }, + }, + }; + + await ids.reduce(async (col, id) => { + await col; + const ip = await indexPatterns.get(id); + + const scriptedFields = ip.getScriptedFields(); + const runtimeFields = ip.fields.filter((fld) => !!fld.runtimeField); + + if (scriptedFields.length > 0) { + // increment counts + results.indexPatternsWithScriptedFieldCount++; + results.scriptedFieldCount += scriptedFields.length; + + // calc LoC + results.perIndexPattern.scriptedFieldLineCount = minMaxAvgLoC( + scriptedFields.map((fld) => fld.script || '') + ); + + // calc field counts + results.perIndexPattern.scriptedFieldCount.min = updateMin( + results.perIndexPattern.scriptedFieldCount.min, + scriptedFields.length + ); + results.perIndexPattern.scriptedFieldCount.max = updateMax( + results.perIndexPattern.scriptedFieldCount.max, + scriptedFields.length + ); + results.perIndexPattern.scriptedFieldCount.avg = + results.scriptedFieldCount / results.indexPatternsWithScriptedFieldCount; + } + + if (runtimeFields.length > 0) { + // increment counts + results.indexPatternsWithRuntimeFieldCount++; + results.runtimeFieldCount += runtimeFields.length; + + // calc LoC + const runtimeFieldScripts = runtimeFields.map( + (fld) => fld.runtimeField?.script?.source || '' + ); + results.perIndexPattern.runtimeFieldLineCount = minMaxAvgLoC(runtimeFieldScripts); + + // calc field counts + results.perIndexPattern.runtimeFieldCount.min = updateMin( + results.perIndexPattern.runtimeFieldCount.min, + runtimeFields.length + ); + results.perIndexPattern.runtimeFieldCount.max = updateMax( + results.perIndexPattern.runtimeFieldCount.max, + runtimeFields.length + ); + results.perIndexPattern.runtimeFieldCount.avg = + results.runtimeFieldCount / results.indexPatternsWithRuntimeFieldCount; + } + }, Promise.resolve()); + + return results; +} + +export function registerIndexPatternsUsageCollector( + getStartServices: StartServicesAccessor, + usageCollection?: UsageCollectionSetup +): void { + if (!usageCollection) { + return; + } + + const indexPatternUsageCollector = usageCollection.makeUsageCollector({ + type: 'index-patterns', + isReady: () => true, + fetch: async () => { + const [{ savedObjects, elasticsearch }, , { indexPatterns }] = await getStartServices(); + const indexPatternService = await indexPatterns.indexPatternsServiceFactory( + new SavedObjectsClient(savedObjects.createInternalRepository()), + elasticsearch.client.asInternalUser + ); + + return await getIndexPatternTelemetry(indexPatternService); + }, + schema: { + indexPatternsCount: { type: 'long' }, + indexPatternsWithScriptedFieldCount: { type: 'long' }, + indexPatternsWithRuntimeFieldCount: { type: 'long' }, + scriptedFieldCount: { type: 'long' }, + runtimeFieldCount: { type: 'long' }, + perIndexPattern: { + scriptedFieldCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + runtimeFieldCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + scriptedFieldLineCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + runtimeFieldLineCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + }, + }, + }); + + usageCollection.registerCollector(indexPatternUsageCollector); +} diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index a7a7663d6981c..7b73802f1a34d 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -46,8 +46,10 @@ export interface DataPluginSetupDependencies { usageCollection?: UsageCollectionSetup; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataPluginStartDependencies {} +export interface DataPluginStartDependencies { + fieldFormats: FieldFormatsStart; + logger: Logger; +} export class DataServerPlugin implements @@ -82,7 +84,11 @@ export class DataServerPlugin this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); - this.indexPatterns.setup(core, { expressions }); + this.indexPatterns.setup(core, { + expressions, + logger: this.logger.get('indexPatterns'), + usageCollection, + }); core.uiSettings.register(getUiSettings()); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index ac81c5b0d5df4..96927728f2f2f 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -74,7 +74,6 @@ export class AggsService { const { calculateAutoTimeExpression, - getDateMetaByDatatableColumn, datatableUtilities, types, } = this.aggsCommonService.start({ @@ -119,7 +118,6 @@ export class AggsService { return { calculateAutoTimeExpression, - getDateMetaByDatatableColumn, datatableUtilities, createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts index 70864d57b3565..3644a3c13c48d 100644 --- a/src/plugins/data/server/search/aggs/mocks.ts +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -58,7 +58,6 @@ export const searchAggsSetupMock = (): AggsSetup => ({ const commonStartMock = (): AggsCommonStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), - getDateMetaByDatatableColumn: jest.fn(), datatableUtilities: { getIndexPattern: jest.fn(), getAggConfig: jest.fn(), diff --git a/src/plugins/data/server/search/expressions/esdsl.ts b/src/plugins/data/server/search/expressions/esdsl.ts new file mode 100644 index 0000000000000..e16204c8782e4 --- /dev/null +++ b/src/plugins/data/server/search/expressions/esdsl.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { StartServicesAccessor } from 'src/core/server'; +import { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { getEsdslFn } from '../../../common/search/expressions/esdsl'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getEsdsl({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getEsdslFn({ + getStartDependencies: async (getKibanaRequest: any) => { + const [core, , { search }] = await getStartServices(); + if (!getKibanaRequest || !getKibanaRequest()) { + throw new Error('TODO: add text'); + } + const request = getKibanaRequest(); + const savedObjectsClient = core.savedObjects.getScopedClient(request); + return { + uiSettingsClient: core.uiSettings.asScopedToClient(savedObjectsClient), + search: search.asScoped(request).search, + }; + }, + }); +} diff --git a/src/plugins/data/server/search/expressions/index.ts b/src/plugins/data/server/search/expressions/index.ts index a052066186235..bb690c2e6e7c6 100644 --- a/src/plugins/data/server/search/expressions/index.ts +++ b/src/plugins/data/server/search/expressions/index.ts @@ -7,3 +7,4 @@ */ export * from './esaggs'; +export * from './esdsl'; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 192c133c94a04..d5a83efcc215f 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -10,7 +10,7 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import { CoreSetup, CoreStart, SavedObject } from '../../../../core/server'; import { coreMock } from '../../../../core/server/mocks'; -import { DataPluginStart } from '../plugin'; +import { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { createFieldFormatsStartMock } from '../field_formats/mocks'; import { createIndexPatternsStartMock } from '../index_patterns/mocks'; @@ -32,7 +32,7 @@ import { createSearchSessionsClientMock } from './mocks'; describe('Search service', () => { let plugin: SearchService; - let mockCoreSetup: MockedKeys>; + let mockCoreSetup: MockedKeys>; let mockCoreStart: MockedKeys; beforeEach(() => { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index fdf0b66197b34..e53244fa7ff26 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -38,7 +38,7 @@ import { FieldFormatsStart } from '../field_formats'; import { IndexPatternsServiceStart } from '../index_patterns'; import { getCallMsearch, registerMsearchRoute, registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; -import { DataPluginStart } from '../plugin'; +import { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; @@ -63,8 +63,9 @@ import { searchSourceRequiredUiSettings, SearchSourceService, phraseFilterFunction, + esRawResponse, } from '../../common/search'; -import { getEsaggs } from './expressions'; +import { getEsaggs, getEsdsl } from './expressions'; import { getShardDelayBucketAgg, SHARD_DELAY_AGG_NAME, @@ -113,7 +114,7 @@ export class SearchService implements Plugin { } public setup( - core: CoreSetup<{}, DataPluginStart>, + core: CoreSetup, { bfetch, expressions, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { const usage = usageCollection ? usageProvider(core) : undefined; @@ -150,6 +151,7 @@ export class SearchService implements Plugin { } expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices })); + expressions.registerFunction(getEsdsl({ getStartServices: core.getStartServices })); expressions.registerFunction(kibana); expressions.registerFunction(luceneFunction); expressions.registerFunction(kqlFunction); @@ -162,6 +164,7 @@ export class SearchService implements Plugin { expressions.registerFunction(rangeFilterFunction); expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); + expressions.registerType(esRawResponse); const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 12458d7a74d9f..9fff4ac95c87e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -73,6 +73,7 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiCounterMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; +import { UsageCollectionSetup as UsageCollectionSetup_2 } from 'src/plugins/usage_collection/server'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -202,6 +203,10 @@ export interface AggFunctionsMapping { // // (undocumented) aggSignificantTerms: ReturnType; + // Warning: (ae-forgotten-export) The symbol "aggSinglePercentile" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggSinglePercentile: ReturnType; // Warning: (ae-forgotten-export) The symbol "aggStdDeviation" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -403,7 +408,7 @@ export const ES_SEARCH_STRATEGY = "es"; // Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>; +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_36, Arguments_21, Output_36>; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -954,16 +959,14 @@ export { IndexPatternsService } // // @public (undocumented) export class IndexPatternsServiceProvider implements Plugin_3 { - // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts // // (undocumented) - setup(core: CoreSetup_2, { expressions }: IndexPatternsServiceSetupDeps): void; - // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts - // + setup(core: CoreSetup_2, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1163,6 +1166,8 @@ export enum METRIC_TYPES { // (undocumented) SERIAL_DIFF = "serial_diff", // (undocumented) + SINGLE_PERCENTILE = "single_percentile", + // (undocumented) STD_DEV = "std_dev", // (undocumented) SUM = "sum", @@ -1207,6 +1212,7 @@ export type ParsedInterval = ReturnType; export function parseInterval(interval: string): moment.Duration | null; // Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DataServerPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1226,7 +1232,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -1297,7 +1303,7 @@ export const search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; @@ -1516,7 +1522,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts index 29c93886ebba3..5d2be533065e7 100644 --- a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts +++ b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts @@ -24,45 +24,46 @@ describe('applyAggsToSearchSource', () => { const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock); expect(aggsConfig!.aggs).toMatchInlineSnapshot(` - Array [ - Object { - "enabled": true, - "id": "1", - "params": Object {}, - "schema": "metric", - "type": "count", - }, - Object { - "enabled": true, - "id": "2", - "params": Object { - "drop_partials": false, - "extended_bounds": Object {}, - "field": "timestamp", - "interval": "auto", - "min_doc_count": 1, - "scaleMetricValues": false, - "useNormalizedEsInterval": true, + Array [ + Object { + "enabled": true, + "id": "1", + "params": Object {}, + "schema": "metric", + "type": "count", }, - "schema": "segment", - "type": "date_histogram", - }, - ] - `); + Object { + "enabled": true, + "id": "2", + "params": Object { + "drop_partials": false, + "extended_bounds": Object {}, + "field": "timestamp", + "interval": "auto", + "min_doc_count": 1, + "scaleMetricValues": false, + "useNormalizedEsInterval": true, + "used_interval": "0ms", + }, + "schema": "segment", + "type": "date_histogram", + }, + ] + `); expect(setField).toHaveBeenCalledWith('aggs', expect.any(Function)); const dslFn = setField.mock.calls[0][1]; expect(dslFn()).toMatchInlineSnapshot(` - Object { - "2": Object { - "date_histogram": Object { - "field": "timestamp", - "min_doc_count": 1, - "time_zone": "America/New_York", - }, - }, - } - `); + Object { + "2": Object { + "date_histogram": Object { + "field": "timestamp", + "min_doc_count": 1, + "time_zone": "America/New_York", + }, + }, + } + `); }); test('enabled = false', () => { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx index 6d5c2224c0635..feabc107dcf3d 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { Component } from 'react'; +import React, { Component, FormEvent } from 'react'; import { EuiFormRow, @@ -72,80 +72,87 @@ export class CustomizePanelModal extends Component {

- - -

Customize panel

-
-
+
{ + event.preventDefault(); + this.save(); + }} + > + + +

Customize panel

+
+
- - - - } - onChange={this.onHideTitleToggle} - /> - - - this.setState({ title: e.target.value })} - aria-label={i18n.translate( - 'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel', + + + + } + onChange={this.onHideTitleToggle} + /> + + - - - } - /> - - - - this.props.cancel()}> - - + > + this.setState({ title: e.target.value })} + aria-label={i18n.translate( + 'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel', + { + defaultMessage: 'Enter a custom title for your panel', + } + )} + append={ + + + + } + /> + + + + this.props.cancel()}> + + - - - - + + + + +
diff --git a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts index a75d6f6c06a95..ed9e588a999b4 100644 --- a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts @@ -7,15 +7,14 @@ */ import { i18n } from '@kbn/i18n'; - export const cloudPasswordAndResetLink = i18n.translate( 'home.tutorials.common.cloudInstructions.passwordAndResetLink', { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.' + - `\\{#config.cloud.resetPasswordUrl\\} - Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.resetPasswordUrl\\}). - \\{/config.cloud.resetPasswordUrl\\}`, + `\\{#config.cloud.profileUrl\\} + Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.baseUrl\\}\\{config.cloud.profileUrl\\}). + \\{/config.cloud.profileUrl\\}`, values: { passwordTemplate: '``' }, } ); diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 15511e1e9f7f2..1402416d69c96 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ -import { - ConfigDeprecationLogger, - CoreSetup, - CoreStart, - PluginConfigDescriptor, -} from 'kibana/server'; +import { AddConfigDeprecation, CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; import { get } from 'lodash'; import { configSchema, ConfigSchema } from '../config'; @@ -23,17 +18,28 @@ export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: ({ renameFromRoot }) => [ // TODO: Remove deprecation once defaultAppId is deleted - renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), - (completeConfig: Record, rootPath: string, log: ConfigDeprecationLogger) => { + renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', { silent: true }), + ( + completeConfig: Record, + rootPath: string, + addDeprecation: AddConfigDeprecation + ) => { if ( get(completeConfig, 'kibana.defaultAppId') === undefined && get(completeConfig, 'kibana_legacy.defaultAppId') === undefined ) { return completeConfig; } - log( - `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead` - ); + addDeprecation({ + message: `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead`, + correctiveActions: { + manualSteps: [ + 'Go to Stack Management > Advanced Settings', + 'Update the "defaultRoute" setting under the General section', + 'Remove "kibana.defaultAppId" from the kibana.yml config file', + ], + }, + }); return completeConfig; }, ], diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index fa0a32fc3d542..0054bb9c01b41 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -347,20 +347,6 @@ class TableListView extends React.Component - ); - } - } - renderToolsLeft() { const selection = this.state.selectedIds; @@ -473,10 +459,9 @@ class TableListView extends React.Component = { type: 'keyword', _meta: { description: 'Default value of the setting was changed.' }, }, + 'banners:textContent': { + type: 'keyword', + _meta: { description: 'Default value of the setting was changed.' }, + }, // non-sensitive 'visualize:enableLabs': { type: 'boolean', @@ -408,6 +412,18 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'banners:placement': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, + 'banners:textColor': { + type: 'text', + _meta: { description: 'Non-default value of setting.' }, + }, + 'banners:backgroundColor': { + type: 'text', + _meta: { description: 'Non-default value of setting.' }, + }, 'observability:enableAlertingExperience': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index c4a70f5065d8e..810f13931225f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -18,6 +18,7 @@ export interface UsageStats { 'timelion:graphite.url': string; 'xpackDashboardMode:roles': string; 'securitySolution:ipReputationLinks': string; + 'banners:textContent': string; /** * non-sensitive settings */ @@ -114,4 +115,7 @@ export interface UsageStats { 'csv:quoteValues': boolean; 'dateFormat:dow': string; dateFormat: string; + 'banners:placement': string; + 'banners:textColor': string; + 'banners:backgroundColor': string; } diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.test.js b/src/plugins/maps_ems/public/service_settings/service_settings.test.js index 5bd371aace79b..eb67997c253b9 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.test.js +++ b/src/plugins/maps_ems/public/service_settings/service_settings.test.js @@ -103,43 +103,8 @@ describe('service_settings (FKA tile_map test)', function () { expect(tmsService.attribution.includes('OpenStreetMap')).toEqual(true); }); - describe('modify - url', function () { - let tilemapServices; - + describe('tms mods', function () { let serviceSettings; - async function assertQuery(expected) { - const attrs = await serviceSettings.getAttributesForTMSLayer(tilemapServices[0]); - const urlObject = url.parse(attrs.url, true); - Object.keys(expected).forEach((key) => { - expect(urlObject.query[key]).toEqual(expected[key]); - }); - } - - it('accepts an object', async () => { - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar' }); - }); - - it('merged additions with previous values', async () => { - // ensure that changes are always additive - serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'bar', bar: 'stool' }); - }); - - it('overwrites conflicting previous values', async () => { - serviceSettings = makeServiceSettings(); - // ensure that conflicts are overwritten - serviceSettings.setQueryParams({ foo: 'bar' }); - serviceSettings.setQueryParams({ bar: 'stool' }); - serviceSettings.setQueryParams({ foo: 'tstool' }); - tilemapServices = await serviceSettings.getTMSServices(); - await assertQuery({ foo: 'tstool', bar: 'stool' }); - }); it('should merge in tilemap url', async () => { serviceSettings = makeServiceSettings( @@ -161,7 +126,7 @@ describe('service_settings (FKA tile_map test)', function () { id: 'road_map', name: 'Road Map - Bright', url: - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl', minZoom: 0, maxZoom: 10, attribution: @@ -208,19 +173,19 @@ describe('service_settings (FKA tile_map test)', function () { ); expect(desaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationFalse.maxZoom).toEqual(10); expect(desaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(desaturationTrue.maxZoom).toEqual(18); expect(darkThemeDesaturationFalse.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationFalse.maxZoom).toEqual(22); expect(darkThemeDesaturationTrue.url).toEqual( - 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3&license=sspl' ); expect(darkThemeDesaturationTrue.maxZoom).toEqual(22); }); @@ -264,14 +229,13 @@ describe('service_settings (FKA tile_map test)', function () { describe('File layers', function () { it('should load manifest (all props)', async function () { const serviceSettings = makeServiceSettings(); - serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); const urlObject = url.parse(fileUrl, true); - Object.keys({ foo: 'bar', elastic_tile_service_tos: 'agree' }).forEach((key) => { + Object.keys({ elastic_tile_service_tos: 'agree' }).forEach((key) => { expect(typeof urlObject.query[key]).toEqual('string'); }); }); diff --git a/src/plugins/maps_ems/public/service_settings/service_settings.ts b/src/plugins/maps_ems/public/service_settings/service_settings.ts index f7c735b6c3037..412db42a1570c 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings.ts @@ -22,7 +22,6 @@ export class ServiceSettings implements IServiceSettings { private readonly _mapConfig: MapsEmsConfig; private readonly _tilemapsConfig: TileMapConfig; private readonly _hasTmsConfigured: boolean; - private _showZoomMessage: boolean; private readonly _emsClient: EMSClient; private readonly tmsOptionsFromConfig: any; @@ -31,7 +30,6 @@ export class ServiceSettings implements IServiceSettings { this._tilemapsConfig = tilemapsConfig; this._hasTmsConfigured = typeof tilemapsConfig.url === 'string' && tilemapsConfig.url !== ''; - this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), appVersion: getKibanaVersion(), @@ -45,6 +43,9 @@ export class ServiceSettings implements IServiceSettings { return fetch(...args); }, }); + // any kibana user, regardless of distribution, should get all zoom levels + // use `sspl` license to indicate this + this._emsClient.addQueryParams({ license: 'sspl' }); const markdownIt = new MarkdownIt({ html: false, @@ -58,18 +59,6 @@ export class ServiceSettings implements IServiceSettings { }); } - shouldShowZoomMessage({ origin }: { origin: string }): boolean { - return origin === ORIGIN.EMS && this._showZoomMessage; - } - - enableZoomMessage(): void { - this._showZoomMessage = true; - } - - disableZoomMessage(): void { - this._showZoomMessage = false; - } - __debugStubManifestCalls(manifestRetrieval: () => Promise): { removeStub: () => void } { const oldGetManifest = this._emsClient.getManifest; diff --git a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts index 80a9aae835844..6b04bd200eba8 100644 --- a/src/plugins/maps_ems/public/service_settings/service_settings_types.ts +++ b/src/plugins/maps_ems/public/service_settings/service_settings_types.ts @@ -46,8 +46,6 @@ export interface IServiceSettings { getFileLayers(): Promise; getUrlForRegionLayer(layer: FileLayer): Promise; setQueryParams(params: { [p: string]: string }): void; - enableZoomMessage(): void; - disableZoomMessage(): void; getAttributesForTMSLayer( tmsServiceConfig: TmsLayer, isDesaturated: boolean, diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json index 8e283288e34b2..f321274791a3b 100644 --- a/src/plugins/maps_legacy/kibana.json +++ b/src/plugins/maps_legacy/kibana.json @@ -5,5 +5,5 @@ "ui": true, "server": true, "requiredPlugins": ["mapsEms"], - "requiredBundles": ["kibanaReact", "visDefaultEditor", "mapsEms"] + "requiredBundles": ["visDefaultEditor", "mapsEms"] } diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 9cd574c5246e8..a261bcf6edd80 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -193,13 +193,12 @@ export function BaseMapsVisualizationProvider() { isDesaturated, isDarkMode ); - const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._kibanaMap.setBaseLayer({ baseLayerType: 'tms', - options: { ...options, showZoomMessage, ...meta }, + options: { ...options, ...meta }, }); } diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index eea8315419289..62dbbda2588a5 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -7,13 +7,11 @@ */ import { EventEmitter } from 'events'; -import { createZoomWarningMsg } from './map_messages'; import $ from 'jquery'; import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../maps_ems/common'; -import { getToasts } from '../kibana_services'; import { L } from '../leaflet'; function makeFitControl(fitContainer, kibanaMap) { @@ -479,22 +477,6 @@ export class KibanaMap extends EventEmitter { this._updateLegend(); } - _addMaxZoomMessage = (layer) => { - const zoomWarningMsg = createZoomWarningMsg( - getToasts(), - this.getZoomLevel, - this.getMaxZoomLevel - ); - - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - }; - setLegendPosition(position) { if (this._legendPosition === position) { if (!this._leafletLegendControl) { @@ -572,11 +554,6 @@ export class KibanaMap extends EventEmitter { }); this._leafletBaseLayer = baseLayer; - if (settings.options.showZoomMessage) { - baseLayer.on('add', () => { - this._addMaxZoomMessage(baseLayer); - }); - } this._leafletBaseLayer.addTo(this._leafletMap); this._leafletBaseLayer.bringToBack(); if (settings.options.minZoom > this._leafletMap.getZoom()) { diff --git a/src/plugins/maps_legacy/public/map/map_messages.js b/src/plugins/maps_legacy/public/map/map_messages.js deleted file mode 100644 index f60d819f0b390..0000000000000 --- a/src/plugins/maps_legacy/public/map/map_messages.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; -import { toMountPoint } from '../../../kibana_react/public'; - -export const createZoomWarningMsg = (function () { - let disableZoomMsg = false; - const setZoomMsg = (boolDisableMsg) => (disableZoomMsg = boolDisableMsg); - - class ZoomWarning extends React.Component { - constructor(props) { - super(props); - this.state = { - disabled: false, - }; - } - - render() { - return ( -
-

- - {`default distribution `} - - ), - ems: ( - - {`Elastic Maps Service`} - - ), - wms: ( - - {`Custom WMS Configuration`} - - ), - configSettings: ( - - {`Custom TMS Using Config Settings`} - - ), - }} - /> -

- - { - this.setState( - { - disabled: true, - }, - () => this.props.onChange(this.state.disabled) - ); - }} - data-test-subj="suppressZoomWarnings" - > - {`Don't show again`} - -
- ); - } - } - - const zoomToast = { - title: 'No additional zoom levels', - text: toMountPoint(), - 'data-test-subj': 'maxZoomWarning', - }; - - return (toastService, getZoomLevel, getMaxZoomLevel) => { - return () => { - const zoomLevel = getZoomLevel(); - const maxMapZoom = getMaxZoomLevel(); - if (!disableZoomMsg && zoomLevel === maxMapZoom) { - toastService.addDanger(zoomToast); - } - }; - }; -})(); diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 57d05262319f2..4491be04b1a42 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -28,6 +28,7 @@ interface SaveModalDocumentInfo { export interface SaveModalDashboardProps { documentInfo: SaveModalDocumentInfo; + canSaveByReference: boolean; objectType: string; onClose: () => void; onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; @@ -35,7 +36,7 @@ export interface SaveModalDashboardProps { } export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { - const { documentInfo, tagOptions, objectType, onClose } = props; + const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); @@ -49,7 +50,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { documentId || disableDashboardOptions ? null : 'existing' ); const [isAddToLibrarySelected, setAddToLibrary] = useState( - !initialCopyOnSave || disableDashboardOptions + canSaveByReference && (!initialCopyOnSave || disableDashboardOptions) ); const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( null @@ -65,13 +66,16 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onChange={(option) => { setDashboardOption(option); }} + canSaveByReference={canSaveByReference} {...{ copyOnSave, documentId, dashboardOption, setAddToLibrary, isAddToLibrarySelected }} /> ) : null; const onCopyOnSaveChange = (newCopyOnSave: boolean) => { - setAddToLibrary(true); + if (canSaveByReference) { + setAddToLibrary(true); + } setDashboardOption(null); setCopyOnSave(newCopyOnSave); }; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx index dd6fd975f8e07..341f194b71ba4 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx @@ -33,15 +33,21 @@ export default { control: 'boolean', defaultValue: true, }, + canSaveVisualizations: { + control: 'boolean', + defaultValue: true, + }, }, }; export function Example({ copyOnSave, hasDocumentId, + canSaveVisualizations, }: { copyOnSave: boolean; hasDocumentId: boolean; + canSaveVisualizations: boolean; } & StorybookParams) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing'); const [isAddToLibrarySelected, setAddToLibrary] = useState(false); @@ -52,6 +58,7 @@ export function Example({ onChange={setDashboardOption} dashboardOption={dashboardOption} copyOnSave={copyOnSave} + canSaveByReference={canSaveVisualizations} documentId={hasDocumentId ? 'abc' : undefined} isAddToLibrarySelected={isAddToLibrarySelected} setAddToLibrary={setAddToLibrary} diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 1ae54040571a2..78a1569c02ead 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -30,6 +30,7 @@ export interface SaveModalDashboardSelectorProps { copyOnSave: boolean; documentId?: string; onSelectDashboard: DashboardPickerProps['onChange']; + canSaveByReference: boolean; setAddToLibrary: (selected: boolean) => void; isAddToLibrarySelected: boolean; dashboardOption: 'new' | 'existing' | null; @@ -40,6 +41,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp const { documentId, onSelectDashboard, + canSaveByReference, setAddToLibrary, isAddToLibrarySelected, dashboardOption, @@ -114,7 +116,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp setAddToLibrary(true); onChange(null); }} - disabled={isDisabled} + disabled={isDisabled || !canSaveByReference} />
@@ -127,7 +129,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp defaultMessage: 'Add to library', })} checked={isAddToLibrarySelected} - disabled={dashboardOption === null || isDisabled} + disabled={dashboardOption === null || isDisabled || !canSaveByReference} onChange={(event) => setAddToLibrary(event.target.checked)} />
diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 74607b9e35e47..39dae92aa2ba9 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -23,6 +23,7 @@ export interface PresentationDashboardsService { export interface PresentationCapabilitiesService { canAccessDashboards: () => boolean; canCreateNewDashboards: () => boolean; + canSaveVisualizations: () => boolean; } export interface PresentationUtilServices { diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index 546281d083f2f..6949fba00c65a 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -16,10 +16,11 @@ export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< >; export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => { - const { dashboard } = coreStart.application.capabilities; + const { dashboard, visualize } = coreStart.application.capabilities; return { canAccessDashboards: () => Boolean(dashboard.show), canCreateNewDashboards: () => Boolean(dashboard.createNew), + canSaveVisualizations: () => Boolean(visualize.save), }; }; diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index fcd38b29f154c..16fbe3baf488f 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -19,11 +19,13 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ canAccessDashboards, canCreateNewDashboards, canEditDashboards, + canSaveVisualizations, }) => { const check = (value: boolean = true) => value; return { canAccessDashboards: () => check(canAccessDashboards), canCreateNewDashboards: () => check(canCreateNewDashboards), canEditDashboards: () => check(canEditDashboards), + canSaveVisualizations: () => check(canSaveVisualizations), }; }; diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index 37b2171635e96..dd7de54264062 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -18,6 +18,7 @@ export interface StorybookParams { canAccessDashboards?: boolean; canCreateNewDashboards?: boolean; canEditDashboards?: boolean; + canSaveVisualizations?: boolean; } export const providers: PluginServiceProviders = { diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 979ccc8faadd5..4154fa65a0cd7 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -15,4 +15,5 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({ canAccessDashboards: () => true, canCreateNewDashboards: () => true, canEditDashboards: () => true, + canSaveVisualizations: () => true, }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index ee96ae041dd09..05ac1eb84089d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -24,6 +24,81 @@ } } }, + "index-patterns": { + "properties": { + "indexPatternsCount": { + "type": "long" + }, + "indexPatternsWithScriptedFieldCount": { + "type": "long" + }, + "indexPatternsWithRuntimeFieldCount": { + "type": "long" + }, + "scriptedFieldCount": { + "type": "long" + }, + "runtimeFieldCount": { + "type": "long" + }, + "perIndexPattern": { + "properties": { + "scriptedFieldCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "runtimeFieldCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "scriptedFieldLineCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "runtimeFieldLineCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + } + } + } + } + }, "kql": { "properties": { "optInCount": { @@ -7478,6 +7553,12 @@ "description": "Default value of the setting was changed." } }, + "banners:textContent": { + "type": "keyword", + "_meta": { + "description": "Default value of the setting was changed." + } + }, "visualize:enableLabs": { "type": "boolean", "_meta": { @@ -8027,6 +8108,24 @@ "description": "Non-default value of setting." } }, + "banners:placement": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, + "banners:textColor": { + "type": "text", + "_meta": { + "description": "Non-default value of setting." + } + }, + "banners:backgroundColor": { + "type": "text", + "_meta": { + "description": "Non-default value of setting." + } + }, "observability:enableAlertingExperience": { "type": "boolean", "_meta": { diff --git a/src/plugins/timelion/public/directives/timelion_help/timelion_help.html b/src/plugins/timelion/public/directives/timelion_help/timelion_help.html index 2b8706f1f2a81..4c4fdfe4faf51 100644 --- a/src/plugins/timelion/public/directives/timelion_help/timelion_help.html +++ b/src/plugins/timelion/public/directives/timelion_help/timelion_help.html @@ -207,7 +207,7 @@ >

@@ -406,7 +406,7 @@ >

{ - const { savedObjects } = core; - const savedObjectsClient = savedObjects.createInternalRepository(); - - savedObjectsClient - .find({ - type: 'timelion-sheet', - perPage: 1, - }) - .then( - ({ total }) => - total && - logger.warn( - 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html.' - ) - ); -}; +import { getDeprecations, showWarningMessageIfTimelionSheetWasFound } from './deprecations'; export class TimelionPlugin implements Plugin { private logger: Logger; @@ -87,6 +64,8 @@ export class TimelionPlugin implements Plugin { schema: schema.number(), }, }); + + core.deprecations.registerDeprecations({ getDeprecations }); } start(core: CoreStart) { showWarningMessageIfTimelionSheetWasFound(core, this.logger); diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts index 9e2e248c6ccd5..382ef925c5282 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -64,6 +64,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition => '!cumulative_sum', '!geo_bounds', '!filtered_metric', + '!single_percentile', ], aggSettings: { top_hits: { diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index 2f30faa8e9a89..e582f098a5fd5 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -50,7 +50,7 @@ export const tableVisLegacyTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { defaultMessage: 'Metric', }), - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], aggSettings: { top_hits: { allowStrings: true, diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index d645af3180b08..a49748fe86c96 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -46,7 +46,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { defaultMessage: 'Metric', }), - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], aggSettings: { top_hits: { allowStrings: true, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 4052ecbe21997..960122c178caa 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -52,6 +52,7 @@ export const tagCloudVisTypeDefinition = { '!geo_bounds', '!geo_centroid', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_timelion/server/index.ts b/src/plugins/vis_type_timelion/server/index.ts index 1dcb7263c4818..35f4182a50a86 100644 --- a/src/plugins/vis_type_timelion/server/index.ts +++ b/src/plugins/vis_type_timelion/server/index.ts @@ -21,7 +21,7 @@ export const config: PluginConfigDescriptor = { renameFromRoot('timelion_vis.enabled', 'vis_type_timelion.enabled'), renameFromRoot('timelion.enabled', 'vis_type_timelion.enabled'), renameFromRoot('timelion.graphiteUrls', 'vis_type_timelion.graphiteUrls'), - renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', true), + renameFromRoot('timelion.ui.enabled', 'vis_type_timelion.ui.enabled', { silent: true }), ], }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/src/plugins/vis_type_timeseries/common/interval_regexp.test.js b/src/plugins/vis_type_timeseries/common/interval_regexp.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/interval_regexp.test.js rename to src/plugins/vis_type_timeseries/common/interval_regexp.test.ts diff --git a/src/plugins/vis_type_timeseries/common/interval_regexp.js b/src/plugins/vis_type_timeseries/common/interval_regexp.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/interval_regexp.js rename to src/plugins/vis_type_timeseries/common/interval_regexp.ts diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index 1fe6196ad545b..74e247b7af06d 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -33,32 +33,46 @@ export interface FetchedIndexPattern { indexPatternString: string | undefined; } +export type TimeseriesVisData = SeriesData | TableData; + +interface TableData { + type: PANEL_TYPES.TABLE; + uiRestrictions: TimeseriesUIRestrictions; + series?: PanelData[]; + pivot_label?: string; +} + +// series data is not fully typed yet +export type SeriesData = { + type: Exclude; + uiRestrictions: TimeseriesUIRestrictions; +} & { + [key: string]: PanelSeries; +}; + +interface PanelSeries { + annotations: { + [key: string]: unknown[]; + }; + id: string; + series: PanelData[]; + error?: unknown; +} + export interface PanelData { id: string; label: string; data: Array<[number, number]>; + seriesId: string; + splitByLabel: string; + isSplitByTerms: boolean; } -// series data is not fully typed yet -interface SeriesData { - [key: string]: { - annotations: { - [key: string]: unknown[]; - }; - id: string; - series: PanelData[]; - error?: unknown; - }; -} +export const isVisTableData = (data: TimeseriesVisData): data is TableData => + data.type === PANEL_TYPES.TABLE; -export type TimeseriesVisData = SeriesData & { - type: PANEL_TYPES; - uiRestrictions: TimeseriesUIRestrictions; - /** - * series array is responsible only for "table" vis type - */ - series?: unknown[]; -}; +export const isVisSeriesData = (data: TimeseriesVisData): data is SeriesData => + data.type !== PANEL_TYPES.TABLE; export interface SanitizedFieldType { name: string; diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 297b021fa9e77..383b089593565 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -224,6 +224,7 @@ export const panel = schema.object({ gauge_inner_width: stringOrNumberOptionalNullable, gauge_style: stringOptionalNullable, gauge_max: numberOptionalOrEmptyString, + hide_last_value_indicator: schema.boolean(), id: stringRequired, ignore_global_filters: numberOptional, ignore_global_filter: numberOptional, diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index 25965d796e651..d02565717b247 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -48,9 +48,9 @@ export function Agg(props: AggProps) { ...props.style, }; - const indexPattern = - (props.series.override_index_pattern && props.series.series_index_pattern) || - props.panel.index_pattern; + const indexPattern = props.series.override_index_pattern + ? props.series.series_index_pattern + : props.panel.index_pattern; return (

diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js index 19d2b88c8d123..7f93567980b2d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.js @@ -51,8 +51,9 @@ export const FilterRatioAgg = (props) => { (query) => handleChange({ denominator: query }), [handleChange] ); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const defaults = { numerator: getDataStart().query.queryString.getDefaultQuery(), diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index 505a2ff4f3c78..77b2e2f020307 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -39,8 +39,9 @@ export function PercentileAgg(props) { const handleSelectChange = createSelectHandler(handleChange); const handleNumberChange = createNumberHandler(handleChange); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; useEffect(() => { if (!checkModel(model)) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js index a80194f72b7b2..4b1528ca27081 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/positive_rate.js @@ -65,9 +65,9 @@ export const PositiveRateAgg = (props) => { const handleSelectChange = createSelectHandler(handleChange); const htmlId = htmlIdGenerator(); - const indexPattern = - (props.series.override_index_pattern && props.series.series_index_pattern) || - props.panel.index_pattern; + const indexPattern = props.series.override_index_pattern + ? props.series.series_index_pattern + : props.panel.index_pattern; const selectedUnitOptions = UNIT_OPTIONS.filter((o) => o.value === model.unit); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js index 4a4114f70f06a..74b441f446308 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_agg.js @@ -31,8 +31,9 @@ export function StandardAgg(props) { const handleSelectChange = createSelectHandler(handleChange); const restrictFields = getSupportedFieldsByMetricType(model.type); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const htmlId = htmlIdGenerator(); return ( diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js index c28cb294c3308..749a97fa79f28 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/std_deviation.js @@ -72,8 +72,9 @@ const StandardDeviationAggUi = (props) => { const handleSelectChange = createSelectHandler(handleChange); const handleTextChange = createTextHandler(handleChange); - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const htmlId = htmlIdGenerator(); const selectedModeOption = modeOptions.find((option) => { return model.mode === option.value; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js index 9bea32b7cbd5b..92e754c1dcdaf 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/top_hit.js @@ -100,8 +100,9 @@ const TopHitAggUi = (props) => { order: 'desc', }; const model = { ...defaults, ...props.model }; - const indexPattern = - (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; + const indexPattern = series.override_index_pattern + ? series.series_index_pattern + : panel.index_pattern; const aggWithOptionsRestrictFields = [ PANEL_TYPES.TABLE, diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index 8ec35e03c0aec..0ad6344ac51b7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -26,7 +26,8 @@ import { createSelectHandler } from './lib/create_select_handler'; import { createTextHandler } from './lib/create_text_handler'; import { IndexPatternSelect } from './lib/index_pattern_select'; import { YesNo } from './yes_no'; -import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; +import { LastValueModePopover } from './last_value_mode_popover'; +import { KBN_FIELD_TYPES } from '../../../../data/public'; import { FormValidationContext } from '../contexts/form_validation_context'; import { isGteInterval, validateReInterval, isAutoInterval } from './lib/get_interval'; import { i18n } from '@kbn/i18n'; @@ -42,6 +43,7 @@ import { UI_SETTINGS } from '../../../../data/common'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE]; const LEVEL_OF_DETAIL_STEPS = 10; const LEVEL_OF_DETAIL_MIN_BUCKETS = 1; +const HIDE_LAST_VALUE_INDICATOR = 'hide_last_value_indicator'; const validateIntervalValue = (intervalValue) => { const isAutoOrGteInterval = isGteInterval(intervalValue) || isAutoInterval(intervalValue); @@ -129,6 +131,11 @@ export const IndexPattern = ({ updateControlValidity(intervalName, intervalValidation.isValid); }, [intervalName, intervalValidation.isValid, updateControlValidity]); + const toggleIndicatorDisplay = useCallback( + () => onChange({ [HIDE_LAST_VALUE_INDICATOR]: !model.hide_last_value_indicator }), + [model.hide_last_value_indicator, onChange] + ); + return (
{!isTimeSeries && ( @@ -154,6 +161,14 @@ export const IndexPattern = ({ onChange={handleSelectChange(TIME_RANGE_MODE_KEY)} singleSelection={{ asPlainText: true }} isDisabled={disabled} + {...(!isEntireTimeRangeActive(model, isTimeSeries) && { + append: ( + + ), + })} /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx new file mode 100644 index 0000000000000..4ac52a0a80c97 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.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 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexItem, EuiToolTip, EuiFlexGroup, EuiBadge } from '@elastic/eui'; +import { getUISettings } from '../../services'; +import { convertIntervalIntoUnit, isAutoInterval, isGteInterval } from './lib/get_interval'; +import { createIntervalBasedFormatter } from './lib/create_interval_based_formatter'; +import { PanelData } from '../../../common/types'; + +interface LastValueModeIndicatorProps { + seriesData?: PanelData['data']; + panelInterval: number; + modelInterval: string; +} + +const lastValueLabel = i18n.translate('visTypeTimeseries.lastValueModeIndicator.lastValue', { + defaultMessage: 'Last value', +}); + +export const LastValueModeIndicator = ({ + seriesData, + panelInterval, + modelInterval, +}: LastValueModeIndicatorProps) => { + if (!seriesData?.length) return {lastValueLabel}; + + const dateFormat = getUISettings().get('dateFormat'); + const scaledDataFormat = getUISettings().get('dateFormat:scaled'); + + const getFormattedPanelInterval = () => { + const interval = convertIntervalIntoUnit(panelInterval, false); + return interval && `${interval.unitValue}${interval.unitString}`; + }; + + const formatter = createIntervalBasedFormatter(panelInterval, scaledDataFormat, dateFormat); + const lastBucketDate = formatter(seriesData[seriesData.length - 1][0]); + const formattedPanelInterval = + (isAutoInterval(modelInterval) || isGteInterval(modelInterval)) && getFormattedPanelInterval(); + + const tooltipContent = ( + + + + + {formattedPanelInterval && ( + + + + )} + + ); + + return ( + + {}} + onClickAriaLabel={i18n.translate( + 'visTypeTimeseries.lastValueModeIndicator.lastValueModeBadgeAriaLabel', + { + defaultMessage: 'View last value details', + } + )} + > + {lastValueLabel} + + + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss new file mode 100644 index 0000000000000..eac3fa86a2567 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.scss @@ -0,0 +1,7 @@ +.tvbLastValueModePopover { + height: auto; +} + +.tvbLastValueModePopoverBody { + width: 360px; +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx new file mode 100644 index 0000000000000..4124adb6b93fa --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_popover.tsx @@ -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 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 './last_value_mode_popover.scss'; + +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiPopover, EuiPopoverTitle, EuiSwitch } from '@elastic/eui'; + +interface LastValueModePopoverProps { + isIndicatorDisplayed: boolean; + toggleIndicatorDisplay: () => void; +} + +export const LastValueModePopover = ({ + isIndicatorDisplayed, + toggleIndicatorDisplay, +}: LastValueModePopoverProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []); + const closePopover = useCallback(() => setIsPopoverOpen(false), []); + + return ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > +
+ + {i18n.translate('visTypeTimeseries.lastValueModePopover.title', { + defaultMessage: 'Last value options', + })} + + +
+
+ ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts new file mode 100644 index 0000000000000..df0874fdd73ec --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.test.ts @@ -0,0 +1,185 @@ +/* + * Copyright 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 { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; +import { PanelData } from '../../../../common/types'; +import { TimeseriesVisParams } from '../../../types'; +import { convertSeriesToDataTable, addMetaToColumns } from './convert_series_to_datatable'; + +jest.mock('../../../services', () => { + return { + getDataStart: jest.fn(() => { + return { + indexPatterns: jest.fn(), + }; + }), + }; +}); + +describe('convert series to datatables', () => { + let indexPattern: IndexPattern; + + beforeEach(() => { + const fieldMap: Record = { + test1: { name: 'test1', spec: { type: 'date' } } as IndexPatternField, + test2: { name: 'test2' } as IndexPatternField, + test3: { name: 'test3', spec: { type: 'boolean' } } as IndexPatternField, + }; + + const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name]; + indexPattern = { + id: 'index1', + title: 'index1', + timeFieldName: 'timestamp', + getFieldByName, + } as IndexPattern; + }); + + describe('addMetaColumns()', () => { + test('adds the correct meta to a date column', () => { + const columns = [{ id: 0, name: 'test1', isSplit: false }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'count'); + expect(columnsWithMeta).toEqual([ + { + id: '0', + meta: { + field: 'test1', + index: 'index1', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'index1', + type: 'date_histogram', + }, + type: 'date', + }, + name: 'test1', + }, + ]); + }); + + test('adds the correct meta to a non date column', () => { + const columns = [{ id: 1, name: 'Average of test2', isSplit: false }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + expect(columnsWithMeta).toEqual([ + { + id: '1', + meta: { + field: 'Average of test2', + index: 'index1', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'index1', + type: 'avg', + }, + type: 'number', + }, + name: 'Average of test2', + }, + ]); + }); + + test('adds the correct meta for a split column', () => { + const columns = [{ id: 2, name: 'test3', isSplit: true }]; + const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg'); + expect(columnsWithMeta).toEqual([ + { + id: '2', + meta: { + field: 'test3', + index: 'index1', + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: 'index1', + type: 'terms', + }, + type: 'boolean', + }, + name: 'test3', + }, + ]); + }); + }); + + describe('convertSeriesToDataTable()', () => { + const model = { + series: [ + { + formatter: 'number', + id: 'series1', + label: '', + line_width: 1, + metrics: [ + { + field: 'test2', + id: 'series1', + type: 'avg', + }, + ], + split_mode: 'terms', + terms_field: 'Cancelled', + type: 'timeseries', + }, + ], + } as TimeseriesVisParams; + const series = ([ + { + id: 'series1:0', + label: 0, + splitByLabel: 'Average of test2', + labelFormatted: 'false', + data: [ + [1616454000000, 0], + [1616457600000, 5], + [1616461200000, 7], + [1616464800000, 8], + ], + seriesId: 'series1', + isSplitByTerms: true, + }, + { + id: 'series1:1', + label: 1, + splitByLabel: 'Average of test2', + labelFormatted: 'true', + data: [ + [1616454000000, 10], + [1616457600000, 12], + [1616461200000, 1], + [1616464800000, 14], + ], + seriesId: 'series1', + isSplitByTerms: true, + }, + ] as unknown) as PanelData[]; + test('creates one table for one layer series with the correct columns', async () => { + const tables = await convertSeriesToDataTable(model, series, indexPattern); + expect(Object.keys(tables).sort()).toEqual([model.series[0].id].sort()); + + expect(tables.series1.columns.length).toEqual(3); + expect(tables.series1.rows.length).toEqual(8); + }); + + test('the table rows for a series with term aggregation should be a combination of the different terms', async () => { + const tables = await convertSeriesToDataTable(model, series, indexPattern); + expect(Object.keys(tables).sort()).toEqual([model.series[0].id].sort()); + + expect(tables.series1.rows.length).toEqual(8); + const expected1 = series[0].data.map((d) => { + d.push(parseInt(series[0].label, 10)); + return d; + }); + const expected2 = series[1].data.map((d) => { + d.push(parseInt(series[1].label, 10)); + return d; + }); + expect(tables.series1.rows).toEqual([...expected1, ...expected2]); + }); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.ts new file mode 100644 index 0000000000000..164d93e490db1 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_datatable.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 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 { IndexPattern } from 'src/plugins/data/public'; +import { + Datatable, + DatatableRow, + DatatableColumn, + DatatableColumnType, +} from 'src/plugins/expressions/public'; +import { TimeseriesVisParams } from '../../../types'; +import { PanelData } from '../../../../common/types'; +import { fetchIndexPattern } from '../../../../common/index_patterns_utils'; +import { getDataStart } from '../../../services'; +import { X_ACCESSOR_INDEX } from '../../visualizations/constants'; + +interface TSVBTables { + [key: string]: Datatable; +} + +interface TSVBColumns { + id: number; + name: string; + isSplit: boolean; +} + +export const addMetaToColumns = ( + columns: TSVBColumns[], + indexPattern: IndexPattern, + metricsType: string +): DatatableColumn[] => { + return columns.map((column) => { + const field = indexPattern.getFieldByName(column.name); + const type = (field?.spec.type as DatatableColumnType) || 'number'; + const cleanedColumn = { + id: column.id.toString(), + name: column.name, + meta: { + type, + field: column.name, + index: indexPattern.title, + source: 'esaggs', + sourceParams: { + enabled: true, + indexPatternId: indexPattern?.id, + type: type === 'date' ? 'date_histogram' : column.isSplit ? 'terms' : metricsType, + }, + }, + }; + return cleanedColumn; + }); +}; + +export const convertSeriesToDataTable = async ( + model: TimeseriesVisParams, + series: PanelData[], + initialIndexPattern: IndexPattern +) => { + const tables: TSVBTables = {}; + const { indexPatterns } = getDataStart(); + for (let layerIdx = 0; layerIdx < model.series.length; layerIdx++) { + const layer = model.series[layerIdx]; + let usedIndexPattern = initialIndexPattern; + // The user can overwrite the index pattern of a layer. + // In that case, the index pattern should be fetched again. + if (layer.override_index_pattern) { + const { indexPattern } = await fetchIndexPattern(layer.series_index_pattern, indexPatterns); + if (indexPattern) { + usedIndexPattern = indexPattern; + } + } + const isGroupedByTerms = layer.split_mode === 'terms'; + const seriesPerLayer = series.filter((s) => s.seriesId === layer.id); + let id = X_ACCESSOR_INDEX; + + const columns: TSVBColumns[] = [ + { id, name: usedIndexPattern.timeFieldName || '', isSplit: false }, + ]; + if (seriesPerLayer.length) { + id++; + columns.push({ id, name: seriesPerLayer[0].splitByLabel, isSplit: false }); + // Adds an extra column, if the layer is split by terms aggregation + if (isGroupedByTerms) { + id++; + columns.push({ id, name: layer.terms_field || '', isSplit: true }); + } + } + const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern, layer.metrics[0].type); + + let rows: DatatableRow[] = []; + for (let j = 0; j < seriesPerLayer.length; j++) { + const data = seriesPerLayer[j].data.map((rowData) => { + const row: DatatableRow = [rowData[0], rowData[1]]; + // If the layer is split by terms aggregation, the data array should also contain the split value. + if (isGroupedByTerms) { + row.push(seriesPerLayer[j].label); + } + return row; + }); + rows = [...rows, ...data]; + } + tables[layer.id] = { + type: 'datatable', + rows, + columns: columnsWithMeta, + }; + } + return tables; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts similarity index 66% rename from src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts index e2eea5281b197..562aec31a0803 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_xaxis_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts @@ -8,17 +8,21 @@ import moment from 'moment'; -function getFormat(interval, rules = []) { +function getFormat(interval: number, rules: string[][] = []) { for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; - if (!rule[0] || interval >= moment.duration(rule[0])) { + if (!rule[0] || interval >= Number(moment.duration(rule[0]))) { return rule[1]; } } } -export function createXaxisFormatter(interval, rules, dateFormat) { - return (val) => { +export function createIntervalBasedFormatter( + interval: number, + rules: string[][], + dateFormat: string +) { + return (val: moment.MomentInput): string => { return moment(val).format(getFormat(interval, rules) ?? dateFormat); }; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts similarity index 70% rename from src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts index dc297df39a870..9ff0d6832d0cd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_interval.ts @@ -13,6 +13,8 @@ import { search } from '../../../../../../plugins/data/public'; const { parseEsInterval } = search.aggs; import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; import { AUTO_INTERVAL } from '../../../../common/constants'; +import { isVisTableData, PanelData, TimeseriesVisData } from '../../../../common/types'; +import { TimeseriesVisParams } from '../../../types'; export const unitLookup = { s: i18n.translate('visTypeTimeseries.getInterval.secondsLabel', { defaultMessage: 'seconds' }), @@ -24,9 +26,11 @@ export const unitLookup = { y: i18n.translate('visTypeTimeseries.getInterval.yearsLabel', { defaultMessage: 'years' }), }; -export const convertIntervalIntoUnit = (interval, hasTranslateUnitString = true) => { +type TimeUnit = keyof typeof unitLookup; + +export const convertIntervalIntoUnit = (interval: number, hasTranslateUnitString = true) => { // Iterate units from biggest to smallest - const units = Object.keys(unitLookup).reverse(); + const units = Object.keys(unitLookup).reverse() as TimeUnit[]; const duration = moment.duration(interval, 'ms'); for (let i = 0; i < units.length; i++) { @@ -41,11 +45,16 @@ export const convertIntervalIntoUnit = (interval, hasTranslateUnitString = true) } }; -export const isGteInterval = (interval) => GTE_INTERVAL_RE.test(interval); -export const isAutoInterval = (interval) => !interval || interval === AUTO_INTERVAL; +export const isGteInterval = (interval: string) => GTE_INTERVAL_RE.test(interval); +export const isAutoInterval = (interval: string) => !interval || interval === AUTO_INTERVAL; + +interface ValidationResult { + isValid: boolean; + errorMessage?: string; +} -export const validateReInterval = (intervalValue) => { - const validationResult = {}; +export const validateReInterval = (intervalValue: string) => { + const validationResult = {} as ValidationResult; try { parseEsInterval(intervalValue); @@ -58,14 +67,12 @@ export const validateReInterval = (intervalValue) => { return validationResult; }; -export const getInterval = (visData, model) => { - let series; - - if (model && model.type === 'table') { - series = get(visData, `series[0].series`, []); - } else { - series = get(visData, `${model.id}.series`, []); - } +export const getInterval = (visData: TimeseriesVisData, model: TimeseriesVisParams) => { + const series = get( + visData, + isVisTableData(visData) ? `series[0].series` : `${model.id}.series`, + [] + ) as PanelData[]; return series.reduce((currentInterval, item) => { if (item.data.length > 1) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config.js b/src/plugins/vis_type_timeseries/public/application/components/series_config.js index 3185503acb569..8f3893feb89bd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config.js @@ -33,10 +33,9 @@ export const SeriesConfig = (props) => { const handleSelectChange = createSelectHandler(props.onChange); const handleTextChange = createTextHandler(props.onChange); const htmlId = htmlIdGenerator(); - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; + const seriesIndexPattern = props.model.override_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; return (
diff --git a/src/plugins/vis_type_timeseries/public/application/components/split.js b/src/plugins/vis_type_timeseries/public/application/components/split.js index 63aa717174a04..4990800acf6db 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/split.js +++ b/src/plugins/vis_type_timeseries/public/application/components/split.js @@ -63,8 +63,9 @@ export class Split extends Component { render() { const { model, panel, uiRestrictions, seriesQuantity } = this.props; - const indexPattern = - (model.override_index_pattern && model.series_index_pattern) || panel.index_pattern; + const indexPattern = model.override_index_pattern + ? model.series_index_pattern + : panel.index_pattern; const splitMode = get(this.props, 'model.split_mode', SPLIT_MODES.EVERYTHING); const Component = this.getComponent(splitMode, uiRestrictions); diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.scss b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.scss new file mode 100644 index 0000000000000..d76e8b685248f --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.scss @@ -0,0 +1,3 @@ +.tvbLastValueIndicator { + align-self: flex-end; +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index ac15a788d6dab..7fba2e1cb701f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -6,8 +6,13 @@ * Side Public License, v 1. */ +import './timeseries_visualization.scss'; + import React, { useCallback, useEffect } from 'react'; +import { get } from 'lodash'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { IUiSettingsClient } from 'src/core/public'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; @@ -16,8 +21,17 @@ import { PaletteRegistry } from 'src/plugins/charts/public'; // @ts-expect-error import { ErrorComponent } from './error'; import { TimeseriesVisTypes } from './vis_types'; +import { TimeseriesVisData, PanelData, isVisSeriesData } from '../../../common/types'; +import { fetchIndexPattern } from '../../../common/index_patterns_utils'; import { TimeseriesVisParams } from '../../types'; -import { TimeseriesVisData } from '../../../common/types'; +import { getDataStart } from '../../services'; +import { convertSeriesToDataTable } from './lib/convert_series_to_datatable'; +import { X_ACCESSOR_INDEX } from '../visualizations/constants'; +import { LastValueModeIndicator } from './last_value_mode_indicator'; +import { getInterval } from './lib/get_interval'; +import { AUTO_INTERVAL } from '../../../common/constants'; +import { TIME_RANGE_DATA_MODES } from '../../../common/timerange_data_modes'; +import { PANEL_TYPES } from '../../../common/panel_types'; interface TimeseriesVisualizationProps { className?: string; @@ -41,25 +55,29 @@ function TimeseriesVisualization({ palettesService, }: TimeseriesVisualizationProps) { const onBrush = useCallback( - (gte: string, lte: string) => { - handlers.event({ - name: 'applyFilter', + async (gte: string, lte: string, series: PanelData[]) => { + const indexPatternValue = model.index_pattern || ''; + const { indexPatterns } = getDataStart(); + const { indexPattern } = await fetchIndexPattern(indexPatternValue, indexPatterns); + + const tables = indexPattern + ? await convertSeriesToDataTable(model, series, indexPattern) + : null; + const table = tables?.[model.series[0].id]; + + const range: [number, number] = [parseInt(gte, 10), parseInt(lte, 10)]; + const event = { data: { - timeFieldName: '*', - filters: [ - { - range: { - '*': { - gte, - lte, - }, - }, - }, - ], + table, + column: X_ACCESSOR_INDEX, + range, + timeFieldName: indexPattern?.timeFieldName, }, - }); + name: 'brush', + }; + handlers.event(event); }, - [handlers] + [handlers, model] ); const handleUiState = useCallback( @@ -76,7 +94,7 @@ function TimeseriesVisualization({ }); // Show the error panel - const error = visData[model.id]?.error; + const error = isVisSeriesData(visData) && visData[model.id]?.error; if (error) { return (
@@ -87,18 +105,40 @@ function TimeseriesVisualization({ const VisComponent = TimeseriesVisTypes[model.type]; + const isLastValueMode = + !model.time_range_mode || model.time_range_mode === TIME_RANGE_DATA_MODES.LAST_VALUE; + const shouldDisplayLastValueIndicator = + isLastValueMode && !model.hide_last_value_indicator && model.type !== PANEL_TYPES.TIMESERIES; + if (VisComponent) { return ( - + + {shouldDisplayLastValueIndicator && ( + + + + )} + + + + ); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js index bfce125a7ed87..bb264aaacbfbf 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js @@ -8,17 +8,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { get } from 'lodash'; import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import { - getInterval, - convertIntervalIntoUnit, - isAutoInterval, - isGteInterval, -} from './lib/get_interval'; -import { AUTO_INTERVAL } from '../../../common/constants'; -import { PANEL_TYPES } from '../../../common/panel_types'; const MIN_CHART_HEIGHT = 300; @@ -28,7 +19,6 @@ class VisEditorVisualizationUI extends Component { this.state = { height: MIN_CHART_HEIGHT, dragging: false, - panelInterval: 0, }; this._visEl = React.createRef(); @@ -65,18 +55,7 @@ class VisEditorVisualizationUI extends Component { await this._handler.render(this._visEl.current); this.props.eventEmitter.emit('embeddableRendered'); - this._subscription = this._handler.handler.data$.subscribe((data) => { - this.setPanelInterval(data.value.visData); - onDataChange(data.value); - }); - } - - setPanelInterval(visData) { - const panelInterval = getInterval(visData, this.props.model); - - if (this.state.panelInterval !== panelInterval) { - this.setState({ panelInterval }); - } + this._subscription = this._handler.handler.data$.subscribe((data) => onDataChange(data.value)); } /** @@ -98,28 +77,6 @@ class VisEditorVisualizationUI extends Component { } }; - hasShowPanelIntervalValue() { - const type = get(this.props, 'model.type', ''); - const interval = get(this.props, 'model.interval', AUTO_INTERVAL); - - return ( - [ - PANEL_TYPES.METRIC, - PANEL_TYPES.TOP_N, - PANEL_TYPES.GAUGE, - PANEL_TYPES.MARKDOWN, - PANEL_TYPES.TABLE, - ].includes(type) && - (isAutoInterval(interval) || isGteInterval(interval)) - ); - } - - getFormattedPanelInterval() { - const interval = convertIntervalIntoUnit(this.state.panelInterval, false); - - return interval ? `${interval.unitValue}${interval.unitString}` : null; - } - componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouseMove); window.removeEventListener('mouseup', this.handleMouseUp); @@ -154,8 +111,6 @@ class VisEditorVisualizationUI extends Component { style.userSelect = 'none'; } - const panelInterval = this.hasShowPanelIntervalValue() && this.getFormattedPanelInterval(); - let applyMessage = ( - {panelInterval && ( - - -

- -

-
-
- )} -

{applyMessage}

diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 0e169c50e4db6..3447641352468 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -13,7 +13,7 @@ import { PersistedState } from 'src/plugins/visualizations/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { TimeseriesVisParams } from '../../../types'; -import { TimeseriesVisData } from '../../../../common/types'; +import { TimeseriesVisData, PanelData } from '../../../../common/types'; /** * Lazy load each visualization type, since the only one is presented on the screen at the same time. @@ -44,7 +44,7 @@ export const TimeseriesVisTypes: Record void; + onBrush: (gte: string, lte: string, series: PanelData[]) => Promise; onUiState: ( field: string, value: { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js index 22bf2fa4ca708..1c3a0411998b0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -327,10 +327,9 @@ export const TimeseriesConfig = injectI18n(function (props) { const disableSeparateYaxis = model.separate_axis ? false : true; - const seriesIndexPattern = - props.model.override_index_pattern && props.model.series_index_pattern - ? props.model.series_index_pattern - : props.indexPatternForQuery; + const seriesIndexPattern = props.model.override_index_pattern + ? props.model.series_index_pattern + : props.indexPatternForQuery; const initialPalette = { ...model.palette, diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 5a2fc05817f71..ae3fa4d9dcca4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -19,7 +19,7 @@ import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public import { replaceVars } from '../../lib/replace_vars'; import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; -import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; +import { createIntervalBasedFormatter } from '../../lib/create_interval_based_formatter'; import { STACKED_OPTIONS } from '../../../visualizations/constants'; import { getCoreStart } from '../../../../services'; @@ -35,7 +35,11 @@ class TimeseriesVisualization extends Component { dateFormat = this.props.getConfig('dateFormat'); xAxisFormatter = (interval) => (val) => { - const formatter = createXaxisFormatter(interval, this.scaledDataFormat, this.dateFormat); + const formatter = createIntervalBasedFormatter( + interval, + this.scaledDataFormat, + this.dateFormat + ); return formatter(val); }; diff --git a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts similarity index 82% rename from src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js rename to src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts index 016d119ed1675..a602b34d99986 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts @@ -6,12 +6,20 @@ * Side Public License, v 1. */ -import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; import { i18n } from '@kbn/i18n'; +import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; import { search } from '../../../../../plugins/data/public'; + +import type { TimeRangeBounds } from '../../../../data/common'; +import type { TimeseriesVisParams } from '../../types'; + const { parseInterval } = search.aggs; -export function validateInterval(bounds, panel, maxBuckets) { +export function validateInterval( + bounds: TimeRangeBounds, + panel: TimeseriesVisParams, + maxBuckets: number +) { const { interval } = panel; const { min, max } = bounds; // No need to check auto it will return around 100 @@ -20,8 +28,9 @@ export function validateInterval(bounds, panel, maxBuckets) { const greaterThanMatch = interval.match(GTE_INTERVAL_RE); if (greaterThanMatch) return; const duration = parseInterval(interval); + if (duration) { - const span = max.valueOf() - min.valueOf(); + const span = max!.valueOf() - min!.valueOf(); const buckets = Math.floor(span / duration.asMilliseconds()); if (buckets > maxBuckets) { throw new Error( diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/chart.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts similarity index 97% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts index 1bc98c6c2a722..5fd6933fcef01 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/icons.ts @@ -5,8 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +// @ts-expect-error import { bombIcon } from '../../components/svg/bomb_icon'; +// @ts-expect-error import { fireIcon } from '../../components/svg/fire_icon'; export const ICON_NAMES = { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.js rename to src/plugins/vis_type_timeseries/public/application/visualizations/constants/index.ts diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 537344a6da39a..2911a9ee5d6e9 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -100,7 +100,7 @@ export const TimeSeries = ({ return; } const [min, max] = x; - onBrush(min, max); + onBrush(min, max, series); }; const getSeriesColor = useCallback( @@ -149,6 +149,7 @@ export const TimeSeries = ({ tooltip={{ snap: true, type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, + boundary: document.getElementById('app-fixed-viewport') ?? undefined, headerFormatter: tooltipFormatter, }} externalPointerEvents={{ tooltip: { visible: false } }} diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 5d5e082b2b7bb..4e45ddf434771 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -74,7 +74,7 @@ export const metricsVisDefinition = { }, toExpressionAst, getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.applyFilter]; + return [VIS_EVENT_TO_TRIGGER.brush]; }, inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index bf3779674b6ea..bf58287870c82 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -28,14 +28,15 @@ export const metricsRequestHandler = async ({ searchSessionId, }: MetricsRequestHandlerParams): Promise => { const config = getUISettings(); + const data = getDataStart(); + const timezone = getTimezone(config); const uiStateObj = uiState[visParams.type] ?? {}; - const data = getDataStart(); - const dataSearch = getDataStart().search; + const dataSearch = data.search; const parsedTimeRange = data.query.timefilter.timefilter.calculateBounds(input?.timeRange!); if (visParams && visParams.id && !visParams.isModelInvalid) { - const maxBuckets = config.get(MAX_BUCKETS_SETTING); + const maxBuckets = config.get(MAX_BUCKETS_SETTING); validateInterval(parsedTimeRange, visParams, maxBuckets); diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx index c314594aa5420..208d1b3325e3d 100644 --- a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx @@ -9,12 +9,13 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; import { IUiSettingsClient } from 'kibana/public'; import type { PersistedState } from '../../visualizations/public'; import { VisualizationContainer } from '../../visualizations/public'; import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; import { TimeseriesRenderValue } from './metrics_fn'; -import { TimeseriesVisData } from '../common/types'; +import { isVisTableData, TimeseriesVisData } from '../common/types'; import { TimeseriesVisParams } from './types'; import { getChartsSetup } from './services'; @@ -24,7 +25,7 @@ const TimeseriesVisualization = lazy( const checkIfDataExists = (visData: TimeseriesVisData | {}, model: TimeseriesVisParams) => { if ('type' in visData) { - const data = visData.type === 'table' ? visData.series : visData?.[model.id]?.series; + const data = isVisTableData(visData) ? visData.series : visData?.[model.id]?.series; return Boolean(data?.length); } @@ -46,22 +47,24 @@ export const getTimeseriesVisRenderer: (deps: { const palettesService = await palettes.getPalettes(); render( - - + - , + showNoResult={showNoResult} + > + + + , domNode ); }, diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index 37eda1b1338d4..f41a6374462dc 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -15,9 +15,13 @@ export { VisTypeTimeseriesSetup } from './plugin'; export const config: PluginConfigDescriptor = { deprecations: ({ unused, renameFromRoot }) => [ // In Kibana v7.8 plugin id was renamed from 'metrics' to 'vis_type_timeseries': - renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', true), - renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', true), - renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', true), + renameFromRoot('metrics.enabled', 'vis_type_timeseries.enabled', { silent: true }), + renameFromRoot('metrics.chartResolution', 'vis_type_timeseries.chartResolution', { + silent: true, + }), + renameFromRoot('metrics.minimumBucketSize', 'vis_type_timeseries.minimumBucketSize', { + silent: true, + }), // Unused properties which should be removed after releasing Kibana v8.0: unused('chartResolution'), @@ -29,3 +33,5 @@ export const config: PluginConfigDescriptor = { export function plugin(initializerContext: PluginInitializerContext) { return new VisTypeTimeseriesPlugin(initializerContext); } + +export { TimeseriesVisData, isVisSeriesData, isVisTableData } from '../common/types'; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts index e2012c5331516..8029e8684c441 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts @@ -7,20 +7,19 @@ */ import { DefaultSearchCapabilities } from './default_search_capabilities'; -import { VisTypeTimeseriesRequest } from '../../../types'; describe('DefaultSearchCapabilities', () => { let defaultSearchCapabilities: DefaultSearchCapabilities; - let req: VisTypeTimeseriesRequest; beforeEach(() => { - req = {} as VisTypeTimeseriesRequest; - defaultSearchCapabilities = new DefaultSearchCapabilities(req); + defaultSearchCapabilities = new DefaultSearchCapabilities({ + timezone: 'UTC', + maxBucketsLimit: 2000, + }); }); test('should init default search capabilities', () => { - expect(defaultSearchCapabilities.request).toBe(req); - expect(defaultSearchCapabilities.fieldsCapabilities).toEqual({}); + expect(defaultSearchCapabilities.timezone).toBe('UTC'); }); test('should return defaultTimeInterval', () => { @@ -35,18 +34,6 @@ describe('DefaultSearchCapabilities', () => { }); }); - test('should return Search Timezone', () => { - defaultSearchCapabilities.request = ({ - body: { - timerange: { - timezone: 'UTC', - }, - }, - } as unknown) as VisTypeTimeseriesRequest; - - expect(defaultSearchCapabilities.searchTimezone).toEqual('UTC'); - }); - test('should return a valid time interval', () => { expect(defaultSearchCapabilities.getValidTimeInterval('20m')).toBe('20m'); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts index 967b34707de53..b60d2e61e9a43 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts @@ -13,23 +13,20 @@ import { getSuitableUnit, } from '../../vis_data/helpers/unit_to_seconds'; import { RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions'; -import { VisTypeTimeseriesRequest, VisTypeTimeseriesVisDataRequest } from '../../../types'; -const isVisDataRequest = ( - request: VisTypeTimeseriesRequest -): request is VisTypeTimeseriesVisDataRequest => { - return !!(request as VisTypeTimeseriesVisDataRequest).body; -}; - -const getTimezoneFromRequest = (request: VisTypeTimeseriesRequest) => { - if (isVisDataRequest(request)) return request.body.timerange.timezone; -}; +export interface SearchCapabilitiesOptions { + timezone?: string; + maxBucketsLimit: number; +} export class DefaultSearchCapabilities { - constructor( - public request: VisTypeTimeseriesRequest, - public fieldsCapabilities: Record = {} - ) {} + public timezone: SearchCapabilitiesOptions['timezone']; + public maxBucketsLimit: SearchCapabilitiesOptions['maxBucketsLimit']; + + constructor(options: SearchCapabilitiesOptions) { + this.timezone = options.timezone; + this.maxBucketsLimit = options.maxBucketsLimit; + } public get defaultTimeInterval() { return null; @@ -55,10 +52,6 @@ export class DefaultSearchCapabilities { }; } - public get searchTimezone() { - return getTimezoneFromRequest(this.request); - } - createUiRestriction(restrictionsObject?: Record) { return { '*': !restrictionsObject, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts index bb79c61d2b032..7426c74dc2426 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts @@ -8,13 +8,11 @@ import { Unit } from '@elastic/datemath'; import { RollupSearchCapabilities } from './rollup_search_capabilities'; -import { VisTypeTimeseriesRequest } from '../../../types'; describe('Rollup Search Capabilities', () => { const testTimeZone = 'time_zone'; const testInterval = '10s'; const rollupIndex = 'rollupIndex'; - const request = ({} as unknown) as VisTypeTimeseriesRequest; let fieldsCapabilities: Record; let rollupSearchCaps: RollupSearchCapabilities; @@ -33,16 +31,19 @@ describe('Rollup Search Capabilities', () => { }, }; - rollupSearchCaps = new RollupSearchCapabilities(request, fieldsCapabilities, rollupIndex); + rollupSearchCaps = new RollupSearchCapabilities( + { maxBucketsLimit: 2000, timezone: 'UTC' }, + fieldsCapabilities, + rollupIndex + ); }); test('should create instance of RollupSearchRequest', () => { - expect(rollupSearchCaps.fieldsCapabilities).toBe(fieldsCapabilities); expect(rollupSearchCaps.rollupIndex).toBe(rollupIndex); }); test('should return the "timezone" for the rollup request', () => { - expect(rollupSearchCaps.searchTimezone).toBe(testTimeZone); + expect(rollupSearchCaps.timezone).toBe(testTimeZone); }); test('should return the default "interval" for the rollup request', () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts index f4dbccc846908..eafd5a0ee1cf4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts @@ -5,26 +5,28 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { get, has } from 'lodash'; import { leastCommonInterval, isCalendarInterval } from '../lib/interval_helper'; -import { DefaultSearchCapabilities } from './default_search_capabilities'; -import { VisTypeTimeseriesRequest } from '../../../types'; +import { + DefaultSearchCapabilities, + SearchCapabilitiesOptions, +} from './default_search_capabilities'; export class RollupSearchCapabilities extends DefaultSearchCapabilities { rollupIndex: string; availableMetrics: Record; constructor( - req: VisTypeTimeseriesRequest, + options: SearchCapabilitiesOptions, fieldsCapabilities: Record, rollupIndex: string ) { - super(req, fieldsCapabilities); + super(options); this.rollupIndex = rollupIndex; this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {}); + this.timezone = get(this.dateHistogram, 'time_zone', null); } public get dateHistogram() { @@ -46,10 +48,6 @@ export class RollupSearchCapabilities extends DefaultSearchCapabilities { ); } - public get searchTimezone() { - return get(this.dateHistogram, 'time_zone', null); - } - public get whiteListedMetrics() { const baseRestrictions = this.createUiRestriction({ count: this.createUiRestriction(), diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index a6e7c5b11ee64..f6114a4117bb8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -25,7 +25,15 @@ class MockSearchStrategy extends AbstractSearchStrategy { } describe('SearchStrategyRegister', () => { - const requestContext = {} as VisTypeTimeseriesRequestHandlerContext; + const requestContext = ({ + core: { + uiSettings: { + client: { + get: jest.fn(), + }, + }, + }, + } as unknown) as VisTypeTimeseriesRequestHandlerContext; let registry: SearchStrategyRegistry; beforeAll(() => { @@ -44,7 +52,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a DefaultSearchStrategy instance', async () => { - const req = {} as VisTypeTimeseriesRequest; + const req = { body: {} } as VisTypeTimeseriesRequest; const { searchStrategy, capabilities } = (await registry.getViableStrategy( requestContext, @@ -65,7 +73,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a MockSearchStrategy instance', async () => { - const req = {} as VisTypeTimeseriesRequest; + const req = { body: {} } as VisTypeTimeseriesRequest; const anotherSearchStrategy = new MockSearchStrategy(); registry.addStrategy(anotherSearchStrategy); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts index fb66e32447c22..7add5cb4a4553 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts @@ -33,6 +33,9 @@ describe('AbstractSearchStrategy', () => { asCurrentUser: jest.fn(), }, }, + uiSettings: { + client: jest.fn(), + }, }, search: { search: jest.fn().mockReturnValue(from(Promise.resolve({}))), diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts index d7a4e6ddedc89..9fa79c7b80f8c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts @@ -13,12 +13,23 @@ import { import { DefaultSearchStrategy } from './default_search_strategy'; describe('DefaultSearchStrategy', () => { - const requestContext = {} as VisTypeTimeseriesRequestHandlerContext; + const requestContext = ({ + core: { + uiSettings: { + client: { + get: jest.fn(), + }, + }, + }, + } as unknown) as VisTypeTimeseriesRequestHandlerContext; + let defaultSearchStrategy: DefaultSearchStrategy; let req: VisTypeTimeseriesVisDataRequest; beforeEach(() => { - req = {} as VisTypeTimeseriesVisDataRequest; + req = { + body: {}, + } as VisTypeTimeseriesVisDataRequest; defaultSearchStrategy = new DefaultSearchStrategy(); }); @@ -32,9 +43,11 @@ describe('DefaultSearchStrategy', () => { const value = await defaultSearchStrategy.checkForViability(requestContext, req); expect(value.isViable).toBe(true); - expect(value.capabilities).toEqual({ - request: req, - fieldsCapabilities: {}, - }); + expect(value.capabilities).toMatchInlineSnapshot(` + DefaultSearchCapabilities { + "maxBucketsLimit": undefined, + "timezone": undefined, + } + `); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index f95bf81b5c1d3..17451f7e5777e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -15,15 +15,21 @@ import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequest, } from '../../../types'; +import { MAX_BUCKETS_SETTING } from '../../../../common/constants'; export class DefaultSearchStrategy extends AbstractSearchStrategy { async checkForViability( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesRequest ) { + const uiSettings = requestContext.core.uiSettings.client; + return { isViable: true, - capabilities: new DefaultSearchCapabilities(req), + capabilities: new DefaultSearchCapabilities({ + timezone: req.body.timerange?.timezone, + maxBucketsLimit: await uiSettings.get(MAX_BUCKETS_SETTING), + }), }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts index e6333ca420e0d..ec6f2a7c21af6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts @@ -17,6 +17,7 @@ import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from '../../../types'; +import { MAX_BUCKETS_SETTING } from '../../../../common/constants'; const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); @@ -62,6 +63,7 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { ) { const rollupData = await this.getRollupData(requestContext, indexPatternString); const rollupIndices = getRollupIndices(rollupData); + const uiSettings = requestContext.core.uiSettings.client; isViable = rollupIndices.length === 1; @@ -69,7 +71,13 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { const [rollupIndex] = rollupIndices; const fieldsCapabilities = getCapabilitiesForRollupIndices(rollupData); - capabilities = new RollupSearchCapabilities(req, fieldsCapabilities, rollupIndex); + capabilities = new RollupSearchCapabilities( + { + maxBucketsLimit: await uiSettings.get(MAX_BUCKETS_SETTING), + }, + fieldsCapabilities, + rollupIndex + ); } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js index f9ac4ea2088c0..b9ce76f7176b4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js @@ -30,6 +30,10 @@ const calculateBucketData = (timeInterval, capabilities) => { bucketSize = 1; } + if (bucketSize > capabilities.maxBucketsLimit) { + bucketSize = capabilities.maxBucketsLimit; + } + // Check decimal if (parsedInterval && parsedInterval.value % 1 !== 0) { if (parsedInterval.unit !== 'ms') { @@ -61,16 +65,16 @@ const calculateBucketSizeForAutoInterval = (req, maxBars) => { return search.aggs.calcAutoIntervalLessThan(maxBars, timerange).asSeconds(); }; -export const getBucketSize = (req, interval, capabilities, maxBars) => { - const bucketSize = calculateBucketSizeForAutoInterval(req, maxBars); - let intervalString = `${bucketSize}s`; +export const getBucketSize = (req, interval, capabilities, bars) => { + const defaultBucketSize = calculateBucketSizeForAutoInterval(req, bars); + let intervalString = `${defaultBucketSize}s`; const gteAutoMatch = Boolean(interval) && interval.match(GTE_INTERVAL_RE); if (gteAutoMatch) { const bucketData = calculateBucketData(gteAutoMatch[1], capabilities); - if (bucketData.bucketSize >= bucketSize) { + if (bucketData.bucketSize >= defaultBucketSize) { return bucketData; } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js index aa5b55f7e0e7d..2ca77da2cc641 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js @@ -18,43 +18,49 @@ describe('getBucketSize', () => { }, }; + const capabilities = { + timezone: 'UTC', + maxBucketsLimit: 200000, + getValidTimeInterval: jest.fn((v) => v), + }; + test('returns auto calculated buckets', () => { - const result = getBucketSize(req, 'auto', undefined, 100); + const result = getBucketSize(req, 'auto', capabilities, 100); expect(result).toHaveProperty('bucketSize', 30); expect(result).toHaveProperty('intervalString', '30s'); }); test('returns overridden buckets (1s)', () => { - const result = getBucketSize(req, '1s', undefined, 100); + const result = getBucketSize(req, '1s', capabilities, 100); expect(result).toHaveProperty('bucketSize', 1); expect(result).toHaveProperty('intervalString', '1s'); }); test('returns overridden buckets (10m)', () => { - const result = getBucketSize(req, '10m', undefined, 100); + const result = getBucketSize(req, '10m', capabilities, 100); expect(result).toHaveProperty('bucketSize', 600); expect(result).toHaveProperty('intervalString', '10m'); }); test('returns overridden buckets (1d)', () => { - const result = getBucketSize(req, '1d', undefined, 100); + const result = getBucketSize(req, '1d', capabilities, 100); expect(result).toHaveProperty('bucketSize', 86400); expect(result).toHaveProperty('intervalString', '1d'); }); test('returns overridden buckets (>=2d)', () => { - const result = getBucketSize(req, '>=2d', undefined, 100); + const result = getBucketSize(req, '>=2d', capabilities, 100); expect(result).toHaveProperty('bucketSize', 86400 * 2); expect(result).toHaveProperty('intervalString', '2d'); }); test('returns overridden buckets (>=10s)', () => { - const result = getBucketSize(req, '>=10s', undefined, 100); + const result = getBucketSize(req, '>=10s', capabilities, 100); expect(result).toHaveProperty('bucketSize', 30); expect(result).toHaveProperty('intervalString', '30s'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js index f22226e03a5aa..268c26115233e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js @@ -45,6 +45,7 @@ export async function getSplits(resp, panel, series, meta, extractFields) { const bucket = _.get(resp, `aggregations.${series.id}.buckets.${filter.id}`); bucket.id = `${series.id}:${filter.id}`; bucket.key = filter.id; + bucket.splitByLabel = splitByLabel; bucket.color = filter.color; bucket.label = filter.label || filter.filter.query || '*'; bucket.meta = meta; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index e2ae404d98970..d26bfa9be893e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -257,6 +257,7 @@ describe('getSplits(resp, panel, series)', () => { key: 'filter-1', label: '200s', meta: { bucketSize: 10 }, + splitByLabel: 'Count', color: '#F00', timeseries: { buckets: [] }, }, @@ -264,6 +265,7 @@ describe('getSplits(resp, panel, series)', () => { id: 'SERIES:filter-2', key: 'filter-2', label: '300s', + splitByLabel: 'Count', meta: { bucketSize: 10 }, color: '#0F0', timeseries: { buckets: [] }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js index 48b33c1e787e9..22a475a9997a7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -32,7 +32,7 @@ export function dateHistogram( barTargetUiSettings ); const { from, to } = getTimerange(req); - const timezone = capabilities.searchTimezone; + const { timezone } = capabilities; overwrite(doc, `aggs.${annotation.id}.date_histogram`, { field: timeField, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index dab9a24d06c0f..a9b4f99fdb693 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -37,7 +37,7 @@ export function dateHistogram( const getDateHistogramForLastBucketMode = () => { const { from, to } = offsetTime(req, series.offset_time); - const timezone = capabilities.searchTimezone; + const { timezone } = capabilities; overwrite(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { field: timeField, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index 945c57b2341f3..a1fd242dc150e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -23,7 +23,6 @@ describe('dateHistogram(req, panel, series)', () => { req = { body: { timerange: { - timezone: 'UTC', min: '2017-01-01T00:00:00Z', max: '2017-01-01T01:00:00Z', }, @@ -40,7 +39,7 @@ describe('dateHistogram(req, panel, series)', () => { queryStringOptions: {}, }; indexPattern = {}; - capabilities = new DefaultSearchCapabilities(req); + capabilities = new DefaultSearchCapabilities({ timezone: 'UTC', maxBucketsLimit: 2000 }); uiSettings = { get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50), }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js index 7f126a39e4f3c..db5767f29d090 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js @@ -50,7 +50,7 @@ describe('metricBuckets(req, panel, series)', () => { }, {}, {}, - undefined, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, { get: async () => 50, } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js index cecfee7c660f5..b79e8de13062c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js @@ -46,14 +46,30 @@ describe('positiveRate(req, panel, series)', () => { test('calls next when finished', async () => { const next = jest.fn(); - await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + await positiveRate( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns positive rate aggs', async () => { const next = (doc) => doc; - const doc = await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + const doc = await positiveRate( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(doc).toEqual({ aggs: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js index 8c9179f12824d..16cb01c510bf3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js @@ -51,13 +51,29 @@ describe('siblingBuckets(req, panel, series)', () => { test('calls next when finished', async () => { const next = jest.fn(); - await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + await siblingBuckets( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns sibling aggs', async () => { const next = (doc) => doc; - const doc = await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + const doc = await siblingBuckets( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(doc).toEqual({ aggs: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index 5518065643172..aff1bd5041be5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -32,7 +32,7 @@ export function dateHistogram(req, panel, esQueryConfig, indexPattern, capabilit barTargetUiSettings ); const { from, to } = getTimerange(req); - const timezone = capabilities.searchTimezone; + const { timezone } = capabilities; panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index d97af8ac748f4..89d87da5f3d7e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -72,7 +72,7 @@ describe('buildRequestBody(req)', () => { const series = panel.series[0]; const getValidTimeInterval = jest.fn(() => '10s'); const capabilities = { - searchTimezone: 'UTC', + timezone: 'UTC', getValidTimeInterval, }; const config = { diff --git a/src/plugins/vis_type_timeseries/server/types.ts b/src/plugins/vis_type_timeseries/server/types.ts index 7b42cf61d52b3..2fc46b7cd1f11 100644 --- a/src/plugins/vis_type_timeseries/server/types.ts +++ b/src/plugins/vis_type_timeseries/server/types.ts @@ -23,7 +23,7 @@ export type ConfigObservable = Observable; export type VisTypeTimeseriesRequestHandlerContext = DataRequestHandlerContext; export type VisTypeTimeseriesRouter = IRouter; export type VisTypeTimeseriesVisDataRequest = KibanaRequest<{}, {}, VisPayload>; -export type VisTypeTimeseriesFieldsRequest = KibanaRequest<{}, { index: string }, {}>; +export type VisTypeTimeseriesFieldsRequest = KibanaRequest<{}, { index: string }, any>; export type VisTypeTimeseriesRequest = | VisTypeTimeseriesFieldsRequest | VisTypeTimeseriesVisDataRequest; diff --git a/src/plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson index ace1950f4e909..834bfdc4ff278 100644 --- a/src/plugins/vis_type_vega/public/default.spec.hjson +++ b/src/plugins/vis_type_vega/public/default.spec.hjson @@ -6,7 +6,7 @@ Welcome to Vega visualizations. Here you can design your own dataviz from scrat This example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner. */ - $schema: https://vega.github.io/schema/vega-lite/v4.json + $schema: https://vega.github.io/schema/vega-lite/v5.json title: Event counts from all indexes // Define the data source diff --git a/src/plugins/vis_type_vega/public/test_utils/default.spec.json b/src/plugins/vis_type_vega/public/test_utils/default.spec.json index 8cf763647115f..c1db62fa8035a 100644 --- a/src/plugins/vis_type_vega/public/test_utils/default.spec.json +++ b/src/plugins/vis_type_vega/public/test_utils/default.spec.json @@ -1,5 +1,5 @@ { - "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "title": "Event counts from all indexes", "data": { "url": { @@ -30,7 +30,7 @@ "x": { "field": "key", "type": "temporal", - "axis": { "title": false } + "axis": { "title": null } }, "y": { "field": "doc_count", diff --git a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json index 5394f009b074f..5a5e72f59022b 100644 --- a/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json +++ b/src/plugins/vis_type_vega/public/test_utils/vegalite_graph.json @@ -1,5 +1,5 @@ { - "$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "data": { "format": {"property": "aggregations.time_buckets.buckets"}, "values": { diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index a83409f936078..ce815cba4a4e2 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -20,7 +20,7 @@ const mockedSavedObjects = [ visState: JSON.stringify({ type: 'vega', params: { - spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v4.json" }', + spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v5.json" }', }, }), }, diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index 7e3ff8226fbb6..fa463bea6f27f 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -120,6 +120,7 @@ export const gaugeVisTypeDefinition: VisTypeDefinition = { '!cumulative_sum', '!geo_bounds', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 468651bb4cf4c..e594122871fe7 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -84,6 +84,7 @@ export const goalVisTypeDefinition: VisTypeDefinition = { '!cumulative_sum', '!geo_bounds', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index 8d538399f68b2..f3f320b3658a0 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -95,6 +95,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition = { 'std_dev', 'top_hits', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 59bed0060a6a6..8922f512522a0 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -148,13 +148,15 @@ export const XYSettings: FC = ({ : headerValueFormatter && (tooltip.detailedTooltip ? undefined : ({ value }: any) => headerValueFormatter(value)); + const boundary = document.getElementById('app-fixed-viewport') ?? undefined; const tooltipProps: TooltipProps = tooltip.detailedTooltip ? { ...tooltip, + boundary, customTooltip: tooltip.detailedTooltip(headerFormatter), headerFormatter: undefined, } - : { ...tooltip, headerFormatter }; + : { ...tooltip, boundary, headerFormatter }; return ( { const isDirty = this.handleChanges(); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index 6f1fba26b39b3..e7410c7a97343 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1921,4 +1921,36 @@ describe('migration visualization', () => { expect(migratedTestDoc).toEqual(expectedDoc); }); }); + + describe('7.13.0 tsvb hide Last value indicator by default', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.13.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const createTestDocWithType = (type: string) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{"type":"metrics","params":{"type":"${type}"}}`, + }, + }); + + it('should set hide_last_value_indicator param to true', () => { + const migratedTestDoc = migrate(createTestDocWithType('markdown')); + const hideLastValueIndicator = JSON.parse(migratedTestDoc.attributes.visState).params + .hide_last_value_indicator; + + expect(hideLastValueIndicator).toBeTruthy(); + }); + + it('should ignore timeseries type', () => { + const migratedTestDoc = migrate(createTestDocWithType('timeseries')); + const hideLastValueIndicator = JSON.parse(migratedTestDoc.attributes.visState).params + .hide_last_value_indicator; + + expect(hideLastValueIndicator).toBeUndefined(); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index ced33318413c5..633442ec55d69 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -923,6 +923,34 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => return doc; }; +/** + * [TSVB] Hide Last value indicator by default for all TSVB types except timeseries + */ +const hideTSVBLastValueIndicator: SavedObjectMigrationFn = (doc) => { + try { + const visState = JSON.parse(doc.attributes.visState); + + if (visState && visState.type === 'metrics' && visState.params.type !== 'timeseries') + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify({ + ...visState, + params: { + ...visState.params, + hide_last_value_indicator: true, + }, + }), + }, + }; + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + + return doc; +}; + export const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -958,5 +986,5 @@ export const visualizationSavedObjectTypeMigrations = { '7.10.0': flow(migrateFilterRatioQuery, removeTSVBSearchSource), '7.11.0': flow(enableDataTableVisToolbar), '7.12.0': flow(migrateVislibAreaLineBarTypes, migrateSchema), - '7.13.0': flow(addSupportOfDualIndexSelectionModeInTSVB), + '7.13.0': flow(addSupportOfDualIndexSelectionModeInTSVB, hideTSVBLastValueIndicator), }; diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 4f5679a14b0b7..e696bcb5dbe4d 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -82,6 +82,7 @@ export const getTopNavConfig = ( setActiveUrl, toastNotifications, visualizeCapabilities, + dashboardCapabilities, i18n: { Context: I18nContext }, dashboard, savedObjectsTagging, @@ -205,9 +206,9 @@ export const getTopNavConfig = ( } }; + const allowByValue = dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; const saveButtonLabel = - embeddableId || - (!savedVis.id && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && originatingApp) + embeddableId || (!savedVis.id && allowByValue && originatingApp) ? i18n.translate('visualize.topNavMenu.saveVisualizationToLibraryButtonLabel', { defaultMessage: 'Save to library', }) @@ -219,9 +220,11 @@ export const getTopNavConfig = ( defaultMessage: 'Save', }); - const showSaveAndReturn = - originatingApp && - (savedVis?.id || dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables); + const showSaveAndReturn = originatingApp && (savedVis?.id || allowByValue); + + const showSaveButton = + visualizeCapabilities.save || + (allowByValue && !showSaveAndReturn && dashboardCapabilities.showWriteControls); const topNavMenu: TopNavMenuData[] = [ { @@ -300,7 +303,7 @@ export const getTopNavConfig = ( }, ] : []), - ...(visualizeCapabilities.save + ...(showSaveButton ? [ { id: 'save', @@ -439,7 +442,12 @@ export const getTopNavConfig = ( /> ) : ( { defaultMessage: 'Read only', }), tooltip: i18n.translate('visualize.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save visualizations', + defaultMessage: 'Unable to save visualizations to the library', }), iconType: 'glasses', }); diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index 8cb39feb2e6bb..b3e63e482e734 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - describe('test large number of fields in sidebar', function () { + // FLAKY: https://github.com/elastic/kibana/issues/96113 + describe.skip('test large number of fields in sidebar', function () { before(async function () { await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); await esArchiver.loadIfNeeded('large_fields'); diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index a3daaf8629493..cb4d46f02f56b 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,7 +23,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); - describe('import objects', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/95660 + // FLAKY: https://github.com/elastic/kibana/issues/95706 + describe.skip('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { await esArchiver.load('management'); diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 668aec6ac5783..3af467affa1fb 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -15,7 +15,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); - const testSubjects = getService('testSubjects'); const browser = getService('browser'); const PageObjects = getPageObjects([ 'common', @@ -221,63 +220,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); - - describe('zoom warning behavior', function describeIndexTests() { - // Zoom warning is only applicable to OSS - this.tags(['skipCloud', 'skipFirefox']); - - const waitForLoading = false; - let zoomWarningEnabled; - let last = false; - const toastDefaultLife = 6000; - - before(async function () { - await browser.setWindowSize(1280, 1000); - - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewAggBasedVisualization(); - log.debug('clickTileMap'); - await PageObjects.visualize.clickTileMap(); - await PageObjects.visualize.clickNewSearch(); - - zoomWarningEnabled = await testSubjects.exists('zoomWarningEnabled'); - log.debug(`Zoom warning enabled: ${zoomWarningEnabled}`); - - const zoomLevel = 9; - for (let i = 0; i < zoomLevel; i++) { - await PageObjects.tileMap.clickMapZoomIn(); - } - }); - - beforeEach(async function () { - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - }); - - afterEach(async function () { - if (!last) { - await PageObjects.common.sleep(toastDefaultLife); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - } - }); - - it('should show warning at zoom 10', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should continue providing zoom warning if left alone', async () => { - await testSubjects.existOrFail('maxZoomWarning'); - }); - - it('should suppress zoom warning if suppress warnings button clicked', async () => { - last = true; - await PageObjects.visChart.waitForVisualization(); - await testSubjects.click('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomOut(waitForLoading); - await testSubjects.waitForDeleted('suppressZoomWarnings'); - await PageObjects.tileMap.clickMapZoomIn(waitForLoading); - - await testSubjects.missingOrFail('maxZoomWarning'); - }); - }); }); } diff --git a/test/functional/fixtures/es_archiver/deprecations_service/data.json b/test/functional/fixtures/es_archiver/deprecations_service/data.json new file mode 100644 index 0000000000000..31ce5af20b46c --- /dev/null +++ b/test/functional/fixtures/es_archiver/deprecations_service/data.json @@ -0,0 +1,14 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "test-deprecations-plugin:ff3733a0-9fty-11e7-ahb3-3dcb94193fab", + "source": { + "type": "test-deprecations-plugin", + "updated_at": "2021-02-11T18:51:23.794Z", + "test-deprecations-plugin": { + "title": "Test saved object" + } + } + } +} diff --git a/test/functional/fixtures/es_archiver/deprecations_service/mappings.json b/test/functional/fixtures/es_archiver/deprecations_service/mappings.json new file mode 100644 index 0000000000000..5f7c7e0e7b7dc --- /dev/null +++ b/test/functional/fixtures/es_archiver/deprecations_service/mappings.json @@ -0,0 +1,289 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "query": { + "properties": { + "title": { + "type": "text" + }, + "description": { + "type": "text" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "type": "keyword", + "index": false + } + } + }, + "filters": { + "type": "object", + "enabled": false + }, + "timefilter": { + "type": "object", + "enabled": false + } + } + }, + "test-deprecations-plugin": { + "properties": { + "title": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 14bd002ec9487..699165a51ca8c 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -28,6 +28,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickLinkText(text: string) { await find.clickByDisplayedLinkText(text); } + async clickKibanaSettings() { await testSubjects.click('settings'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -89,6 +90,22 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } + async setAdvancedSettingsTextArea(propertyName: string, propertyValue: string) { + const wrapper = await testSubjects.find(`advancedSetting-editField-${propertyName}`); + const textarea = await wrapper.findByTagName('textarea'); + await textarea.focus(); + // only way to properly replace the value of the ace editor is via the JS api + await browser.execute( + (editor: string, value: string) => { + return (window as any).ace.edit(editor).setValue(value); + }, + `advancedSetting-editField-${propertyName}-editor`, + propertyValue + ); + await testSubjects.click(`advancedSetting-saveButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + async toggleAdvancedSettingCheckbox(propertyName: string) { await testSubjects.click(`advancedSetting-editField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -162,6 +179,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async sortBy(columnName: string) { const chartTypes = await find.allByCssSelector('table.euiTable thead tr th button'); + async function getChartType(chart: Record) { const chartString = await chart.getVisibleText(); if (chartString === columnName) { @@ -169,6 +187,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } } + const getChartTypesPromises = chartTypes.map(getChartType); return Promise.all(getChartTypesPromises); } diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index fbb2b101eb3af..3ed5d74808fce 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -154,7 +154,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async getMarkdownText(): Promise { - const el = await find.byCssSelector('.tvbEditorVisualization'); + const el = await find.byCssSelector('.tvbVis'); const text = await el.getVisibleText(); return text; } diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index fc747fcd71f17..1651e213ee82d 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -56,6 +56,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // Required to load new platform plugins via `--plugin-path` flag. '--env.name=development', + '--corePluginDeprecations.oldProperty=hello', + '--corePluginDeprecations.secret=100', + '--corePluginDeprecations.noLongerUsed=still_using', ...plugins.map( (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/kibana.json b/test/plugin_functional/plugins/core_plugin_deprecations/kibana.json new file mode 100644 index 0000000000000..bc251f97bea58 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "corePluginDeprecations", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["corePluginDeprecations"], + "server": true, + "ui": false +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/package.json b/test/plugin_functional/plugins/core_plugin_deprecations/package.json new file mode 100644 index 0000000000000..f14ec933f59b2 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/package.json @@ -0,0 +1,14 @@ +{ + "name": "core_plugin_deprecations", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_plugin_deprecations", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../node_modules/.bin/tsc" + } +} diff --git a/src/legacy/utils/jest.config.js b/test/plugin_functional/plugins/core_plugin_deprecations/public/application.tsx similarity index 50% rename from src/legacy/utils/jest.config.js rename to test/plugin_functional/plugins/core_plugin_deprecations/public/application.tsx index 593c3aec9d0b0..e2166a249e34b 100644 --- a/src/legacy/utils/jest.config.js +++ b/test/plugin_functional/plugins/core_plugin_deprecations/public/application.tsx @@ -6,8 +6,14 @@ * Side Public License, v 1. */ -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/legacy/utils'], +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters } from 'kibana/public'; + +const DeprecationsApp = () =>
Deprcations App
; + +export const renderApp = ({ element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); }; diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/public/index.ts b/test/plugin_functional/plugins/core_plugin_deprecations/public/index.ts new file mode 100644 index 0000000000000..bb6b3f0740b3b --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/public/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. + */ + +import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; +import { + CorePluginDeprecationsPlugin, + CorePluginDeprecationsPluginSetup, + CorePluginDeprecationsPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + CorePluginDeprecationsPluginSetup, + CorePluginDeprecationsPluginStart +> = (context: PluginInitializerContext) => new CorePluginDeprecationsPlugin(context); diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_deprecations/public/plugin.tsx new file mode 100644 index 0000000000000..bf807145e14bf --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/public/plugin.tsx @@ -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 { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/public'; + +declare global { + interface Window { + env?: PluginInitializerContext['env']; + } +} + +export class CorePluginDeprecationsPlugin + implements Plugin { + constructor(pluginContext: PluginInitializerContext) { + window.env = pluginContext.env; + } + public setup(core: CoreSetup) { + core.application.register({ + id: 'core-plugin-deprecations', + title: 'Core Plugin Deprecations', + async mount(params) { + const { renderApp } = await import('./application'); + await core.getStartServices(); + return renderApp(params); + }, + }); + } + + public start() {} + + public stop() {} +} + +export type CorePluginDeprecationsPluginSetup = ReturnType; +export type CorePluginDeprecationsPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts new file mode 100644 index 0000000000000..db4288d26a3d7 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { get } from 'lodash'; +import type { PluginConfigDescriptor } from 'kibana/server'; +import type { ConfigDeprecation } from '@kbn/config'; + +const configSchema = schema.object({ + newProperty: schema.maybe(schema.string({ defaultValue: 'Some string' })), + noLongerUsed: schema.maybe(schema.string()), + secret: schema.maybe(schema.number({ defaultValue: 42 })), +}); + +type ConfigType = TypeOf; + +const configSecretDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { + if (get(settings, 'corePluginDeprecations.secret') !== 42) { + addDeprecation({ + documentationUrl: 'config-secret-doc-url', + message: + 'Kibana plugin funcitonal tests will no longer allow corePluginDeprecations.secret ' + + 'config to be set to anything except 42.', + }); + } + return settings; +}; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('oldProperty', 'newProperty'), + unused('noLongerUsed'), + configSecretDeprecation, + ], +}; diff --git a/src/core/server/legacy/config/index.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/index.ts similarity index 69% rename from src/core/server/legacy/config/index.ts rename to test/plugin_functional/plugins/core_plugin_deprecations/server/index.ts index b674b1386b786..1968c011a327a 100644 --- a/src/core/server/legacy/config/index.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/index.ts @@ -6,4 +6,7 @@ * Side Public License, v 1. */ -export { ensureValidConfiguration } from './ensure_valid_configuration'; +import { CorePluginDeprecationsPlugin } from './plugin'; + +export { config } from './config'; +export const plugin = () => new CorePluginDeprecationsPlugin(); diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts new file mode 100644 index 0000000000000..38565b1e2c0a8 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts @@ -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 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 { Plugin, CoreSetup, GetDeprecationsContext, DeprecationsDetails } from 'kibana/server'; +import { registerRoutes } from './routes'; +async function getDeprecations({ + savedObjectsClient, +}: GetDeprecationsContext): Promise { + const deprecations: DeprecationsDetails[] = []; + const { total } = await savedObjectsClient.find({ type: 'test-deprecations-plugin', perPage: 1 }); + + deprecations.push({ + message: `CorePluginDeprecationsPlugin is a deprecated feature for testing.`, + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, + }); + + if (total > 0) { + deprecations.push({ + message: `SavedObject test-deprecations-plugin is still being used.`, + documentationUrl: 'another-test-url', + level: 'critical', + correctiveActions: {}, + }); + } + + return deprecations; +} + +export class CorePluginDeprecationsPlugin implements Plugin { + public setup(core: CoreSetup, deps: {}) { + registerRoutes(core.http); + core.savedObjects.registerType({ + name: 'test-deprecations-plugin', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + }, + }, + }); + + core.deprecations.registerDeprecations({ getDeprecations }); + } + + public start() {} + public stop() {} +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/routes.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/routes.ts new file mode 100644 index 0000000000000..d6bf065898f93 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/routes.ts @@ -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 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 { HttpServiceSetup } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export function registerRoutes(http: HttpServiceSetup) { + const router = http.createRouter(); + router.post( + { + path: '/api/core_deprecations_resolve/', + validate: { + body: schema.object({ + mockFail: schema.maybe(schema.boolean()), + keyId: schema.maybe(schema.string()), + deprecationDetails: schema.object({ + domainId: schema.string(), + }), + }), + }, + }, + async (context, req, res) => { + const { mockFail, keyId } = req.body; + if (mockFail === true) { + return res.badRequest({ + body: new Error('Mocking api failure'), + }); + } + + if (keyId) { + const client = context.core.savedObjects.getClient(); + await client.delete('test-deprecations-plugin', keyId, { + refresh: true, + }); + } + + return res.ok(); + } + ); +} diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json new file mode 100644 index 0000000000000..3d9d8ca9451d4 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts new file mode 100644 index 0000000000000..c44781ab284c6 --- /dev/null +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -0,0 +1,247 @@ +/* + * Copyright 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 type { DomainDeprecationDetails, DeprecationsGetResponse } from 'src/core/server/types'; +import type { ResolveDeprecationResponse } from 'src/core/public'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + + const CorePluginDeprecationsPluginDeprecations = [ + { + level: 'critical', + message: + '"corePluginDeprecations.oldProperty" is deprecated and has been replaced by "corePluginDeprecations.newProperty"', + correctiveActions: { + manualSteps: [ + 'Replace "corePluginDeprecations.oldProperty" with "corePluginDeprecations.newProperty" in the Kibana config file, CLI flag, or environment variable (in Docker only).', + ], + }, + domainId: 'corePluginDeprecations', + }, + { + level: 'critical', + message: 'corePluginDeprecations.noLongerUsed is deprecated and is no longer used', + correctiveActions: { + manualSteps: [ + 'Remove "corePluginDeprecations.noLongerUsed" from the Kibana config file, CLI flag, or environment variable (in Docker only)', + ], + }, + domainId: 'corePluginDeprecations', + }, + { + level: 'critical', + message: + 'Kibana plugin funcitonal tests will no longer allow corePluginDeprecations.secret config to be set to anything except 42.', + correctiveActions: {}, + documentationUrl: 'config-secret-doc-url', + domainId: 'corePluginDeprecations', + }, + { + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, + domainId: 'corePluginDeprecations', + }, + { + message: 'SavedObject test-deprecations-plugin is still being used.', + documentationUrl: 'another-test-url', + level: 'critical', + correctiveActions: {}, + domainId: 'corePluginDeprecations', + }, + ]; + + describe('deprecations service', () => { + before(() => esArchiver.load('../functional/fixtures/es_archiver/deprecations_service')); + after(() => esArchiver.unload('../functional/fixtures/es_archiver/deprecations_service')); + + describe('GET /api/deprecations/', async () => { + it('returns registered config deprecations and feature deprecations', async () => { + const { body } = await supertest.get('/api/deprecations/').set('kbn-xsrf', 'true'); + + const { deprecations } = body as DeprecationsGetResponse; + expect(Array.isArray(deprecations)).to.be(true); + const corePluginDeprecations = deprecations.filter( + ({ domainId }) => domainId === 'corePluginDeprecations' + ); + + expect(corePluginDeprecations).to.eql(CorePluginDeprecationsPluginDeprecations); + }); + }); + + describe('Public API', () => { + before(async () => await PageObjects.common.navigateToApp('home')); + + it('#getAllDeprecations returns all deprecations plugin deprecations', async () => { + const result = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations.getAllDeprecations().then(cb); + }); + + const corePluginDeprecations = result.filter( + ({ domainId }) => domainId === 'corePluginDeprecations' + ); + + expect(corePluginDeprecations).to.eql(CorePluginDeprecationsPluginDeprecations); + }); + + it('#getDeprecations returns domain deprecations', async () => { + const corePluginDeprecations = await browser.executeAsync( + (cb) => { + return window._coreProvider.start.core.deprecations + .getDeprecations('corePluginDeprecations') + .then(cb); + } + ); + + expect(corePluginDeprecations).to.eql(CorePluginDeprecationsPluginDeprecations); + }); + + describe('resolveDeprecation', () => { + it('fails on missing correctiveActions.api', async () => { + const resolveResult = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + manualSteps: ['Step a', 'Step b'], + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }); + + expect(resolveResult).to.eql({ + reason: 'deprecation has no correctiveAction via api.', + status: 'fail', + }); + }); + + it('fails on bad request from correctiveActions.api', async () => { + const resolveResult = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + api: { + method: 'POST', + path: '/api/core_deprecations_resolve/', + body: { + mockFail: true, + }, + }, + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }); + + expect(resolveResult).to.eql({ + reason: 'Mocking api failure', + status: 'fail', + }); + }); + + it('fails on 404 request from correctiveActions.api', async () => { + const resolveResult = await browser.executeAsync((cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + api: { + method: 'POST', + path: '/api/invalid_route_not_registered/', + body: { + mockFail: true, + }, + }, + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }); + + expect(resolveResult).to.eql({ + reason: 'Not Found', + status: 'fail', + }); + }); + + it('returns { status: ok } on successful correctiveActions.api', async () => { + const savedObjectId = await supertest + .get('/api/saved_objects/_find?type=test-deprecations-plugin') + .set('kbn-xsrf', 'true') + .expect(200) + .then(({ body }) => { + expect(body.total).to.be(1); + return body.saved_objects[0].id; + }); + + const resolveResult = await browser.executeAsync( + (keyId, cb) => { + return window._coreProvider.start.core.deprecations + .resolveDeprecation({ + message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', + documentationUrl: 'test-url', + level: 'warning', + correctiveActions: { + api: { + method: 'POST', + path: '/api/core_deprecations_resolve/', + body: { keyId }, + }, + }, + domainId: 'corePluginDeprecations', + }) + .then(cb); + }, + savedObjectId + ); + + expect(resolveResult).to.eql({ status: 'ok' }); + await supertest + .get('/api/saved_objects/_find?type=test-deprecations-plugin') + .set('kbn-xsrf', 'true') + .expect(200) + .then(({ body }) => { + expect(body.total).to.be(0); + }); + + const { deprecations } = await supertest + .get('/api/deprecations/') + .set('kbn-xsrf', 'true') + .then( + ({ body }): Promise => { + return body; + } + ); + + const deprecation = deprecations.find( + ({ message }) => message === 'SavedObject test-deprecations-plugin is still being used.' + ); + + expect(deprecation).to.eql(undefined); + }); + }); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core/index.ts b/test/plugin_functional/test_suites/core/index.ts index 9baa1ab0b394d..8591c2fdec8dd 100644 --- a/test/plugin_functional/test_suites/core/index.ts +++ b/test/plugin_functional/test_suites/core/index.ts @@ -10,6 +10,7 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('core', function () { + loadTestFile(require.resolve('./deprecations')); loadTestFile(require.resolve('./route')); }); } diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index dab12de2cef6b..d0a714d6759b5 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -9,7 +9,7 @@ import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils'; import testSubjSelector from '@kbn/test-subj-selector'; import { Test } from '@kbn/test/types/ftr'; -import { pkg } from '../../../../src/core/server/utils'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; // @ts-ignore internal js that is passed to the browser as is @@ -45,6 +45,7 @@ export async function VisualTestingProvider({ getService }: FtrProviderContext) }); const statsCache = new WeakMap(); + function getStats(test: Test) { if (!statsCache.has(test)) { statsCache.set(test, { diff --git a/tsconfig.base.json b/tsconfig.base.json index 865806cffe5bb..da4de5ef3712b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -38,6 +38,8 @@ "moduleResolution": "node", // "resolveJsonModule" allows for importing, extracting types from and generating .json files. "resolveJsonModule": true, + // Do not resolve symlinks to their real path; treat a symlinked file like a real one. + "preserveSymlinks": true, // Disallow inconsistently-cased references to the same file. "forceConsistentCasingInFileNames": false, // Forbid unused local variables as the rule was deprecated by ts-lint diff --git a/tsconfig.json b/tsconfig.json index 18647153acb0a..40763ede1bbdd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -108,7 +108,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, @@ -123,6 +122,7 @@ { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./x-pack/plugins/timelines/tsconfig.json" }, { "path": "./x-pack/plugins/transform/tsconfig.json" }, { "path": "./x-pack/plugins/translations/tsconfig.json" }, { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 2d9ddc1b9e568..f13455a14b4df 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -85,7 +85,6 @@ { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, diff --git a/vars/runbld.groovy b/vars/runbld.groovy index e52bc244c65cb..80416d4fa9a41 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,8 +1,8 @@ def call(script, label, enableJunitProcessing = false) { - def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" + // def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" sh( - script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", + script: "bash ${script}", label: label ?: script ) } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 663ae32f9128a..6bbbf6cd6b82d 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -53,6 +53,7 @@ "xpack.spaces": "plugins/spaces", "xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"], "xpack.taskManager": "legacy/plugins/task_manager", + "xpack.timelines": "plugins/timelines", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", diff --git a/x-pack/package.json b/x-pack/package.json index 14c59cf89a74e..9e96388145038 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -37,7 +37,6 @@ "@kbn/utility-types": "link:../packages/kbn-utility-types" }, "dependencies": { - "@elastic/datemath": "link:../packages/elastic-datemath", "@elastic/safer-lodash-set": "link:../packages/elastic-safer-lodash-set", "@kbn/config-schema": "link:../packages/kbn-config-schema", "@kbn/i18n": "link:../packages/kbn-i18n", diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index a9a0149e72ce7..e340f8bf19126 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -12,6 +12,7 @@ "infra" ], "optionalPlugins": [ + "spaces", "cloud", "usageCollection", "taskManager", diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index ef67501ec761b..1e368b2eb5368 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -24,7 +24,7 @@ import { import { useLayerList } from './useLayerList'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import type { RenderTooltipContentParams } from '../../../../../../maps/public'; import { MapToolTip } from './MapToolTip'; import { useMapFilters } from './useMapFilters'; import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx index 7e6c8ddd493bf..7501d5bfaa2c5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx @@ -20,7 +20,7 @@ import { TRANSACTION_DURATION_COUNTRY, TRANSACTION_DURATION_REGION, } from './useLayerList'; -import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import type { RenderTooltipContentParams } from '../../../../../../maps/public'; import { I18LABELS } from '../translations'; type MapToolTipProps = Partial; diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index 76020d0b48073..75f3cca05c5c5 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -24,12 +24,10 @@ import { ImpactBar } from '../../shared/ImpactBar'; import { useUiTracker } from '../../../../../observability/public'; type CorrelationsApiResponse = - | APIReturnType<'GET /api/apm/correlations/failed_transactions'> - | APIReturnType<'GET /api/apm/correlations/slow_transactions'>; + | APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> + | APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; -type SignificantTerm = NonNullable< - NonNullable['significantTerms'] ->[0]; +type SignificantTerm = CorrelationsApiResponse['significantTerms'][0]; export type SelectedSignificantTerm = Pick< SignificantTerm, diff --git a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index c3b5f52dd84b7..7fb7444a52f84 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -34,8 +34,12 @@ import { useFieldNames } from './use_field_names'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useUiTracker } from '../../../../../observability/public'; +type OverallErrorsApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'> +>; + type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/failed_transactions'> + APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'> >; interface Props { @@ -65,11 +69,41 @@ export function ErrorCorrelations({ onClose }: Props) { ); const hasFieldNames = fieldNames.length > 0; - const { data, status } = useFetcher( + const { data: overallData, status: overallStatus } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', + params: { + query: { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }, + }, + }); + } + }, + [ + environment, + kuery, + serviceName, + start, + end, + transactionName, + transactionType, + ] + ); + + const { data: correlationsData, status: correlationsStatus } = useFetcher( (callApmApi) => { if (start && end && hasFieldNames) { return callApmApi({ - endpoint: 'GET /api/apm/correlations/failed_transactions', + endpoint: 'GET /api/apm/correlations/errors/failed_transactions', params: { query: { environment, @@ -125,8 +159,9 @@ export function ErrorCorrelations({ onClose }: Props) {
@@ -136,8 +171,12 @@ export function ErrorCorrelations({ onClose }: Props) { 'xpack.apm.correlations.error.percentageColumnName', { defaultMessage: '% of failed transactions' } )} - significantTerms={hasFieldNames ? data?.significantTerms : []} - status={status} + significantTerms={ + hasFieldNames && correlationsData?.significantTerms + ? correlationsData.significantTerms + : [] + } + status={correlationsStatus} setSelectedSignificantTerm={setSelectedSignificantTerm} onFilter={onClose} /> @@ -151,10 +190,9 @@ export function ErrorCorrelations({ onClose }: Props) { } function getSelectedTimeseries( - data: CorrelationsApiResponse, + significantTerms: CorrelationsApiResponse['significantTerms'], selectedSignificantTerm: SelectedSignificantTerm ) { - const { significantTerms } = data; if (!significantTerms) { return []; } @@ -168,11 +206,13 @@ function getSelectedTimeseries( } function ErrorTimeseriesChart({ - data, + overallData, + correlationsData, selectedSignificantTerm, status, }: { - data?: CorrelationsApiResponse; + overallData?: OverallErrorsApiResponse; + correlationsData?: CorrelationsApiResponse; selectedSignificantTerm: SelectedSignificantTerm | null; status: FETCH_STATUS; }) { @@ -180,7 +220,7 @@ function ErrorTimeseriesChart({ const dateFormatter = timeFormatter('HH:mm:ss'); return ( - + @@ -206,11 +246,11 @@ function ErrorTimeseriesChart({ yScaleType={ScaleType.Linear} xAccessor={'x'} yAccessors={['y']} - data={data?.overall?.timeseries ?? []} + data={overallData?.overall?.timeseries ?? []} curve={CurveType.CURVE_MONOTONE_X} /> - {data && selectedSignificantTerm ? ( + {correlationsData && selectedSignificantTerm ? ( ) : null} diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 77571421ed00e..e65bad8088c17 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -32,8 +32,12 @@ import { useFieldNames } from './use_field_names'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useUiTracker } from '../../../../../observability/public'; +type OverallLatencyApiResponse = NonNullable< + APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'> +>; + type CorrelationsApiResponse = NonNullable< - APIReturnType<'GET /api/apm/correlations/slow_transactions'> + APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'> >; interface Props { @@ -71,11 +75,45 @@ export function LatencyCorrelations({ onClose }: Props) { 75 ); - const { data, status } = useFetcher( + const { data: overallData, status: overallStatus } = useFetcher( (callApmApi) => { - if (start && end && hasFieldNames) { + if (start && end) { return callApmApi({ - endpoint: 'GET /api/apm/correlations/slow_transactions', + endpoint: 'GET /api/apm/correlations/latency/overall_distribution', + params: { + query: { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }, + }, + }); + } + }, + [ + environment, + kuery, + serviceName, + start, + end, + transactionName, + transactionType, + ] + ); + + const maxLatency = overallData?.maxLatency; + const distributionInterval = overallData?.distributionInterval; + const fieldNamesCommaSeparated = fieldNames.join(','); + + const { data: correlationsData, status: correlationsStatus } = useFetcher( + (callApmApi) => { + if (start && end && hasFieldNames && maxLatency && distributionInterval) { + return callApmApi({ + endpoint: 'GET /api/apm/correlations/latency/slow_transactions', params: { query: { environment, @@ -86,7 +124,9 @@ export function LatencyCorrelations({ onClose }: Props) { start, end, durationPercentile: durationPercentile.toString(10), - fieldNames: fieldNames.join(','), + fieldNames: fieldNamesCommaSeparated, + maxLatency: maxLatency.toString(10), + distributionInterval: distributionInterval.toString(10), }, }, }); @@ -101,8 +141,10 @@ export function LatencyCorrelations({ onClose }: Props) { transactionName, transactionType, durationPercentile, - fieldNames, + fieldNamesCommaSeparated, hasFieldNames, + maxLatency, + distributionInterval, ] ); @@ -134,8 +176,13 @@ export function LatencyCorrelations({ onClose }: Props) { @@ -147,8 +194,12 @@ export function LatencyCorrelations({ onClose }: Props) { 'xpack.apm.correlations.latency.percentageColumnName', { defaultMessage: '% of slow transactions' } )} - significantTerms={hasFieldNames ? data?.significantTerms : []} - status={status} + significantTerms={ + hasFieldNames && correlationsData + ? correlationsData?.significantTerms + : [] + } + status={correlationsStatus} setSelectedSignificantTerm={setSelectedSignificantTerm} onFilter={onClose} /> @@ -167,25 +218,23 @@ export function LatencyCorrelations({ onClose }: Props) { ); } -function getDistributionYMax(data?: CorrelationsApiResponse) { - if (!data?.overall) { - return 0; +function getAxisMaxes(data?: OverallLatencyApiResponse) { + if (!data?.overallDistribution) { + return { xMax: 0, yMax: 0 }; } - - const yValues = [ - ...data.overall.distribution.map((p) => p.y ?? 0), - ...data.significantTerms.flatMap((term) => - term.distribution.map((p) => p.y ?? 0) - ), - ]; - return Math.max(...yValues); + const { overallDistribution } = data; + const xValues = overallDistribution.map((p) => p.x ?? 0); + const yValues = overallDistribution.map((p) => p.y ?? 0); + return { + xMax: Math.max(...xValues), + yMax: Math.max(...yValues), + }; } function getSelectedDistribution( - data: CorrelationsApiResponse, + significantTerms: CorrelationsApiResponse['significantTerms'], selectedSignificantTerm: SelectedSignificantTerm ) { - const { significantTerms } = data; if (!significantTerms) { return []; } @@ -199,23 +248,22 @@ function getSelectedDistribution( } function LatencyDistributionChart({ - data, + overallData, + correlationsData, selectedSignificantTerm, status, }: { - data?: CorrelationsApiResponse; + overallData?: OverallLatencyApiResponse; + correlationsData?: CorrelationsApiResponse['significantTerms']; selectedSignificantTerm: SelectedSignificantTerm | null; status: FETCH_STATUS; }) { const theme = useTheme(); - const xMax = Math.max( - ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) - ); + const { xMax, yMax } = getAxisMaxes(overallData); const durationFormatter = getDurationFormatter(xMax); - const yMax = getDistributionYMax(data); return ( - + { const start = durationFormatter(obj.value); const end = durationFormatter( - obj.value + data?.distributionInterval + obj.value + overallData?.distributionInterval ); return `${start.value} - ${end.formatted}`; @@ -254,12 +302,12 @@ function LatencyDistributionChart({ xAccessor={'x'} yAccessors={['y']} color={theme.eui.euiColorVis1} - data={data?.overall?.distribution || []} + data={overallData?.overallDistribution || []} minBarHeight={5} tickFormat={(d) => `${roundFloat(d)}%`} /> - {data && selectedSignificantTerm ? ( + {correlationsData && selectedSignificantTerm ? ( `${roundFloat(d)}%`} /> diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index 1821e92ee5a78..29fabc51fd582 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -46,11 +46,14 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 10, - transactionCoordinates: [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 3 }, - ], + transactionPerMinute: { + value: 2, + timeseries: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -81,7 +84,7 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [], + transactionPerMinute: { value: null, timeseries: [] }, }) ); const response = await fetchObservabilityOverviewPageData(params); @@ -108,7 +111,10 @@ describe('Observability dashboard data', () => { callApmApiMock.mockImplementation(() => Promise.resolve({ serviceCount: 0, - transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + transactionPerMinute: { + value: 0, + timeseries: [{ x: 1 }, { x: 2 }, { x: 3 }], + }, }) ); const response = await fetchObservabilityOverviewPageData(params); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 55ead8d942aca..3a02efd05e5a5 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { mean } from 'lodash'; import { ApmFetchDataResponse, FetchDataParams, @@ -31,7 +30,7 @@ export const fetchObservabilityOverviewPageData = async ({ }, }); - const { serviceCount, transactionCoordinates } = data; + const { serviceCount, transactionPerMinute } = data; return { appLink: `/app/apm/services?rangeFrom=${relativeTime.start}&rangeTo=${relativeTime.end}`, @@ -42,17 +41,12 @@ export const fetchObservabilityOverviewPageData = async ({ }, transactions: { type: 'number', - value: - mean( - transactionCoordinates - .map(({ y }) => y) - .filter((y) => y && isFinite(y)) - ) || 0, + value: transactionPerMinute.value || 0, }, }, series: { transactions: { - coordinates: transactionCoordinates, + coordinates: transactionPerMinute.timeseries, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts similarity index 63% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts rename to x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index c668f3bb28713..8ee469c9a93c7 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty, omit, merge } from 'lodash'; +import { isEmpty, omit } from 'lodash'; import { EventOutcome } from '../../../../common/event_outcome'; import { processSignificantTermAggs, @@ -13,65 +13,25 @@ import { } from '../process_significant_term_aggs'; import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { - environmentQuery, - rangeQuery, - kqlQuery, -} from '../../../../server/utils/queries'; -import { - EVENT_OUTCOME, - SERVICE_NAME, - TRANSACTION_NAME, - TRANSACTION_TYPE, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { - getOutcomeAggregation, + getTimeseriesAggregation, getTransactionErrorRateTimeSeries, } from '../../helpers/transaction_error_rate'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; -export async function getCorrelationsForFailedTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - fieldNames, - setup, -}: { - environment?: string; - kuery?: string; - serviceName: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; +interface Options extends CorrelationsOptions { fieldNames: string[]; - setup: Setup & SetupTimeRange; -}) { +} +export async function getCorrelationsForFailedTransactions(options: Options) { return withApmSpan('get_correlations_for_failed_transactions', async () => { - const { start, end, apmEventClient } = setup; - - const backgroundFilters: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (serviceName) { - backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } + const { fieldNames, setup } = options; + const { apmEventClient } = setup; + const filters = getCorrelationsFilters(options); const params = { apm: { events: [ProcessorEvent.transaction] }, @@ -79,7 +39,7 @@ export async function getCorrelationsForFailedTransactions({ body: { size: 0, query: { - bool: { filter: backgroundFilters }, + bool: { filter: filters }, }, aggs: { failed_transactions: { @@ -95,7 +55,7 @@ export async function getCorrelationsForFailedTransactions({ field: fieldName, background_filter: { bool: { - filter: backgroundFilters, + filter: filters, must_not: { term: { [EVENT_OUTCOME]: EventOutcome.failure }, }, @@ -112,7 +72,7 @@ export async function getCorrelationsForFailedTransactions({ const response = await apmEventClient.search(params); if (!response.aggregations) { - return {}; + return { significantTerms: [] }; } const sigTermAggs = omit( @@ -121,17 +81,17 @@ export async function getCorrelationsForFailedTransactions({ ); const topSigTerms = processSignificantTermAggs({ sigTermAggs }); - return getErrorRateTimeSeries({ setup, backgroundFilters, topSigTerms }); + return getErrorRateTimeSeries({ setup, filters, topSigTerms }); }); } export async function getErrorRateTimeSeries({ setup, - backgroundFilters, + filters, topSigTerms, }: { setup: Setup & SetupTimeRange; - backgroundFilters: ESFilter[]; + filters: ESFilter[]; topSigTerms: TopSigTerm[]; }) { return withApmSpan('get_error_rate_timeseries', async () => { @@ -139,20 +99,10 @@ export async function getErrorRateTimeSeries({ const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); if (isEmpty(topSigTerms)) { - return {}; + return { significantTerms: [] }; } - const timeseriesAgg = { - date_histogram: { - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: start, max: end }, - }, - aggs: { - outcomes: getOutcomeAggregation(), - }, - }; + const timeseriesAgg = getTimeseriesAggregation(start, end, intervalString); const perTermAggs = topSigTerms.reduce( (acc, term, index) => { @@ -175,8 +125,8 @@ export async function getErrorRateTimeSeries({ apm: { events: [ProcessorEvent.transaction] }, body: { size: 0, - query: { bool: { filter: backgroundFilters } }, - aggs: merge({ timeseries: timeseriesAgg }, perTermAggs), + query: { bool: { filter: filters } }, + aggs: perTermAggs, }, }; @@ -184,15 +134,10 @@ export async function getErrorRateTimeSeries({ const { aggregations } = response; if (!aggregations) { - return {}; + return { significantTerms: [] }; } return { - overall: { - timeseries: getTransactionErrorRateTimeSeries( - aggregations.timeseries.buckets - ), - }, significantTerms: topSigTerms.map((topSig, index) => { const agg = aggregations[`term_${index}`]!; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts new file mode 100644 index 0000000000000..9387e64a51e01 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getTimeseriesAggregation, + getTransactionErrorRateTimeSeries, +} from '../../helpers/transaction_error_rate'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; + +export async function getOverallErrorTimeseries(options: CorrelationsOptions) { + return withApmSpan('get_error_rate_timeseries', async () => { + const { setup } = options; + const filters = getCorrelationsFilters(options); + const { start, end, apmEventClient } = setup; + const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + timeseries: getTimeseriesAggregation(start, end, intervalString), + }, + }, + }; + + const response = await apmEventClient.search(params); + const { aggregations } = response; + + if (!aggregations) { + return { overall: null }; + } + + return { + overall: { + timeseries: getTransactionErrorRateTimeSeries( + aggregations.timeseries.buckets + ), + }, + }; + }); +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts deleted file mode 100644 index 88b1cf3a344ed..0000000000000 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty, dropRightWhile } from 'lodash'; -import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { TopSigTerm } from '../process_significant_term_aggs'; -import { getMaxLatency } from './get_max_latency'; -import { withApmSpan } from '../../../utils/with_apm_span'; - -export async function getLatencyDistribution({ - setup, - backgroundFilters, - topSigTerms, -}: { - setup: Setup & SetupTimeRange; - backgroundFilters: ESFilter[]; - topSigTerms: TopSigTerm[]; -}) { - return withApmSpan('get_latency_distribution', async () => { - const { apmEventClient } = setup; - - if (isEmpty(topSigTerms)) { - return {}; - } - - const maxLatency = await getMaxLatency({ - setup, - backgroundFilters, - topSigTerms, - }); - - if (!maxLatency) { - return {}; - } - - const intervalBuckets = 15; - const distributionInterval = Math.floor(maxLatency / intervalBuckets); - - const distributionAgg = { - // filter out outliers not included in the significant term docs - filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } }, - aggs: { - dist_filtered_by_latency: { - histogram: { - // TODO: add support for metrics - field: TRANSACTION_DURATION, - interval: distributionInterval, - min_doc_count: 0, - extended_bounds: { - min: 0, - max: maxLatency, - }, - }, - }, - }, - }; - - const perTermAggs = topSigTerms.reduce( - (acc, term, index) => { - acc[`term_${index}`] = { - filter: { term: { [term.fieldName]: term.fieldValue } }, - aggs: { - distribution: distributionAgg, - }, - }; - return acc; - }, - {} as Record< - string, - { - filter: AggregationOptionsByType['filter']; - aggs: { - distribution: typeof distributionAgg; - }; - } - > - ); - - const params = { - // TODO: add support for metrics - apm: { events: [ProcessorEvent.transaction] }, - body: { - size: 0, - query: { bool: { filter: backgroundFilters } }, - aggs: { - // overall aggs - distribution: distributionAgg, - - // per term aggs - ...perTermAggs, - }, - }, - }; - - const response = await withApmSpan('get_terms_distribution', () => - apmEventClient.search(params) - ); - type Agg = NonNullable; - - if (!response.aggregations) { - return {}; - } - - function formatDistribution(distribution: Agg['distribution']) { - const total = distribution.doc_count; - - // remove trailing buckets that are empty and out of bounds of the desired number of buckets - const buckets = dropRightWhile( - distribution.dist_filtered_by_latency.buckets, - (bucket, index) => bucket.doc_count === 0 && index > intervalBuckets - 1 - ); - - return buckets.map((bucket) => ({ - x: bucket.key, - y: (bucket.doc_count / total) * 100, - })); - } - - return { - distributionInterval, - overall: { - distribution: formatDistribution(response.aggregations.distribution), - }, - significantTerms: topSigTerms.map((topSig, index) => { - // @ts-expect-error - const agg = response.aggregations[`term_${index}`] as Agg; - - return { - ...topSig, - distribution: formatDistribution(agg.distribution), - }; - }), - }; - }); -} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_filters.ts b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts new file mode 100644 index 0000000000000..92fc9c5d9622b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/get_filters.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { environmentQuery, rangeQuery, kqlQuery } from '../../utils/queries'; +import { + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; + +export interface CorrelationsOptions { + setup: Setup & SetupTimeRange; + environment?: string; + kuery?: string; + serviceName: string | undefined; + transactionType: string | undefined; + transactionName: string | undefined; +} + +export function getCorrelationsFilters({ + setup, + environment, + kuery, + serviceName, + transactionType, + transactionName, +}: CorrelationsOptions) { + const { start, end } = setup; + const correlationsFilters: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; + + if (serviceName) { + correlationsFilters.push({ term: { [SERVICE_NAME]: serviceName } }); + } + + if (transactionType) { + correlationsFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); + } + + if (transactionName) { + correlationsFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); + } + return correlationsFilters; +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts similarity index 63% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts rename to x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts index 9472d385a26c6..0f93d1411a001 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_correlations_for_slow_transactions.ts @@ -6,75 +6,39 @@ */ import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; -import { - environmentQuery, - rangeQuery, - kqlQuery, -} from '../../../../server/utils/queries'; -import { - SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_NAME, - TRANSACTION_TYPE, - PROCESSOR_EVENT, -} from '../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getDurationForPercentile } from './get_duration_for_percentile'; import { processSignificantTermAggs } from '../process_significant_term_aggs'; import { getLatencyDistribution } from './get_latency_distribution'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; -export async function getCorrelationsForSlowTransactions({ - environment, - kuery, - serviceName, - transactionType, - transactionName, - durationPercentile, - fieldNames, - setup, -}: { - environment?: string; - kuery?: string; - serviceName: string | undefined; - transactionType: string | undefined; - transactionName: string | undefined; +interface Options extends CorrelationsOptions { durationPercentile: number; fieldNames: string[]; - setup: Setup & SetupTimeRange; -}) { + maxLatency: number; + distributionInterval: number; +} +export async function getCorrelationsForSlowTransactions(options: Options) { return withApmSpan('get_correlations_for_slow_transactions', async () => { - const { start, end, apmEventClient } = setup; - - const backgroundFilters: ESFilter[] = [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (serviceName) { - backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } }); - } - - if (transactionType) { - backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - - if (transactionName) { - backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - + const { + durationPercentile, + fieldNames, + setup, + maxLatency, + distributionInterval, + } = options; + const { apmEventClient } = setup; + const filters = getCorrelationsFilters(options); const durationForPercentile = await getDurationForPercentile({ durationPercentile, - backgroundFilters, + filters, setup, }); if (!durationForPercentile) { - return {}; + return { significantTerms: [] }; } const response = await withApmSpan('get_significant_terms', () => { @@ -85,7 +49,7 @@ export async function getCorrelationsForSlowTransactions({ query: { bool: { // foreground filters - filter: backgroundFilters, + filter: filters, must: { function_score: { query: { @@ -112,7 +76,7 @@ export async function getCorrelationsForSlowTransactions({ background_filter: { bool: { filter: [ - ...backgroundFilters, + ...filters, { range: { [TRANSACTION_DURATION]: { @@ -132,17 +96,21 @@ export async function getCorrelationsForSlowTransactions({ return apmEventClient.search(params); }); if (!response.aggregations) { - return {}; + return { significantTerms: [] }; } const topSigTerms = processSignificantTermAggs({ sigTermAggs: response.aggregations, }); - return getLatencyDistribution({ + const significantTerms = await getLatencyDistribution({ setup, - backgroundFilters, + filters, topSigTerms, + maxLatency, + distributionInterval, }); + + return { significantTerms }; }); } diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts similarity index 86% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts rename to x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts index 02141f5f9e76f..43c261743861d 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_duration_for_percentile.ts @@ -13,11 +13,11 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getDurationForPercentile({ durationPercentile, - backgroundFilters, + filters, setup, }: { durationPercentile: number; - backgroundFilters: ESFilter[]; + filters: ESFilter[]; setup: Setup & SetupTimeRange; }) { return withApmSpan('get_duration_for_percentiles', async () => { @@ -29,7 +29,7 @@ export async function getDurationForPercentile({ body: { size: 0, query: { - bool: { filter: backgroundFilters }, + bool: { filter: filters }, }, aggs: { percentile: { @@ -42,6 +42,9 @@ export async function getDurationForPercentile({ }, }); - return Object.values(res.aggregations?.percentile.values || {})[0]; + const duration = Object.values( + res.aggregations?.percentile.values || {} + )[0]; + return duration || 0; }); } diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts new file mode 100644 index 0000000000000..6d42b26b22e42 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_latency_distribution.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { TopSigTerm } from '../process_significant_term_aggs'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { + getDistributionAggregation, + trimBuckets, +} from './get_overall_latency_distribution'; + +export async function getLatencyDistribution({ + setup, + filters, + topSigTerms, + maxLatency, + distributionInterval, +}: { + setup: Setup & SetupTimeRange; + filters: ESFilter[]; + topSigTerms: TopSigTerm[]; + maxLatency: number; + distributionInterval: number; +}) { + return withApmSpan('get_latency_distribution', async () => { + const { apmEventClient } = setup; + + const distributionAgg = getDistributionAggregation( + maxLatency, + distributionInterval + ); + + const perTermAggs = topSigTerms.reduce( + (acc, term, index) => { + acc[`term_${index}`] = { + filter: { term: { [term.fieldName]: term.fieldValue } }, + aggs: { + distribution: distributionAgg, + }, + }; + return acc; + }, + {} as Record< + string, + { + filter: AggregationOptionsByType['filter']; + aggs: { + distribution: typeof distributionAgg; + }; + } + > + ); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: perTermAggs, + }, + }; + + const response = await withApmSpan('get_terms_distribution', () => + apmEventClient.search(params) + ); + type Agg = NonNullable; + + if (!response.aggregations) { + return []; + } + + return topSigTerms.map((topSig, index) => { + // ignore the typescript error since existence of response.aggregations is already checked: + // @ts-expect-error + const agg = response.aggregations[`term_${index}`] as Agg[string]; + const total = agg.distribution.doc_count; + const buckets = trimBuckets( + agg.distribution.dist_filtered_by_latency.buckets + ); + + return { + ...topSig, + distribution: buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })), + }; + }); + }); +} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts similarity index 76% rename from x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts rename to x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts index 5f12c86a9c70c..8b415bf0d80a7 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_max_latency.ts @@ -14,12 +14,12 @@ import { TopSigTerm } from '../process_significant_term_aggs'; export async function getMaxLatency({ setup, - backgroundFilters, - topSigTerms, + filters, + topSigTerms = [], }: { setup: Setup & SetupTimeRange; - backgroundFilters: ESFilter[]; - topSigTerms: TopSigTerm[]; + filters: ESFilter[]; + topSigTerms?: TopSigTerm[]; }) { return withApmSpan('get_max_latency', async () => { const { apmEventClient } = setup; @@ -31,13 +31,17 @@ export async function getMaxLatency({ size: 0, query: { bool: { - filter: backgroundFilters, + filter: filters, - // only include docs containing the significant terms - should: topSigTerms.map((term) => ({ - term: { [term.fieldName]: term.fieldValue }, - })), - minimum_should_match: 1, + ...(topSigTerms.length + ? { + // only include docs containing the significant terms + should: topSigTerms.map((term) => ({ + term: { [term.fieldName]: term.fieldValue }, + })), + minimum_should_match: 1, + } + : null), }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts new file mode 100644 index 0000000000000..c5d4def51ea54 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/correlations/latency/get_overall_latency_distribution.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { dropRightWhile } from 'lodash'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { getMaxLatency } from './get_max_latency'; +import { withApmSpan } from '../../../utils/with_apm_span'; +import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; + +export const INTERVAL_BUCKETS = 15; + +export function getDistributionAggregation( + maxLatency: number, + distributionInterval: number +) { + return { + filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } }, + aggs: { + dist_filtered_by_latency: { + histogram: { + // TODO: add support for metrics + field: TRANSACTION_DURATION, + interval: distributionInterval, + min_doc_count: 0, + extended_bounds: { + min: 0, + max: maxLatency, + }, + }, + }, + }, + }; +} + +export async function getOverallLatencyDistribution( + options: CorrelationsOptions +) { + const { setup } = options; + const filters = getCorrelationsFilters(options); + + return withApmSpan('get_overall_latency_distribution', async () => { + const { apmEventClient } = setup; + const maxLatency = await getMaxLatency({ setup, filters }); + if (!maxLatency) { + return { + maxLatency: null, + distributionInterval: null, + overallDistribution: null, + }; + } + const distributionInterval = Math.floor(maxLatency / INTERVAL_BUCKETS); + + const params = { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: { + size: 0, + query: { bool: { filter: filters } }, + aggs: { + // overall distribution agg + distribution: getDistributionAggregation( + maxLatency, + distributionInterval + ), + }, + }, + }; + + const response = await withApmSpan('get_terms_distribution', () => + apmEventClient.search(params) + ); + + if (!response.aggregations) { + return { + maxLatency, + distributionInterval, + overallDistribution: null, + }; + } + + const { distribution } = response.aggregations; + const total = distribution.doc_count; + const buckets = trimBuckets(distribution.dist_filtered_by_latency.buckets); + + return { + maxLatency, + distributionInterval, + overallDistribution: buckets.map((bucket) => ({ + x: bucket.key, + y: (bucket.doc_count / total) * 100, + })), + }; + }); +} + +// remove trailing buckets that are empty and out of bounds of the desired number of buckets +export function trimBuckets(buckets: T[]) { + return dropRightWhile( + buckets, + (bucket, index) => bucket.doc_count === 0 && index > INTERVAL_BUCKETS - 1 + ); +} diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 11d65b7697e9a..b60a2a071e6dc 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -21,6 +21,20 @@ export const getOutcomeAggregation = () => ({ type OutcomeAggregation = ReturnType; +export const getTimeseriesAggregation = ( + start: number, + end: number, + intervalString: string +) => ({ + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + aggs: { outcomes: getOutcomeAggregation() }, +}); + export function calculateTransactionErrorPercentage( outcomeResponse: AggregationResultOf ) { diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts index 1c33fcbd71dac..19163da449b90 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts @@ -36,7 +36,12 @@ describe('createStaticIndexPattern', () => { 'xpack.apm.autocreateApmIndexPattern': false, }); const savedObjectsClient = getMockSavedObjectsClient(); - await createStaticIndexPattern(setup, context, savedObjectsClient); + await createStaticIndexPattern( + setup, + context, + savedObjectsClient, + 'default' + ); expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); @@ -53,7 +58,12 @@ describe('createStaticIndexPattern', () => { const savedObjectsClient = getMockSavedObjectsClient(); - await createStaticIndexPattern(setup, context, savedObjectsClient); + await createStaticIndexPattern( + setup, + context, + savedObjectsClient, + 'default' + ); expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); @@ -70,7 +80,12 @@ describe('createStaticIndexPattern', () => { const savedObjectsClient = getMockSavedObjectsClient(); - await createStaticIndexPattern(setup, context, savedObjectsClient); + await createStaticIndexPattern( + setup, + context, + savedObjectsClient, + 'default' + ); expect(savedObjectsClient.create).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 0b7f82c0b8388..b91fb8342a212 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -20,7 +20,8 @@ import { getApmIndexPatternTitle } from './get_apm_index_pattern_title'; export async function createStaticIndexPattern( setup: Setup, context: APMRequestHandlerContext, - savedObjectsClient: InternalSavedObjectsClient + savedObjectsClient: InternalSavedObjectsClient, + spaceId: string | undefined ): Promise { return withApmSpan('create_static_index_pattern', async () => { const { config } = context; @@ -46,7 +47,11 @@ export async function createStaticIndexPattern( ...apmIndexPattern.attributes, title: apmIndexPatternTitle, }, - { id: APM_STATIC_INDEX_PATTERN_ID, overwrite: false } + { + id: APM_STATIC_INDEX_PATTERN_ID, + overwrite: false, + namespace: spaceId, + } ) ); return true; diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts deleted file mode 100644 index aac18e2bdfe4c..0000000000000 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ /dev/null @@ -1,64 +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 { rangeQuery } from '../../../server/utils/queries'; -import { Coordinates } from '../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -import { calculateThroughput } from '../helpers/calculate_throughput'; -import { withApmSpan } from '../../utils/with_apm_span'; - -export function getTransactionCoordinates({ - setup, - bucketSize, - searchAggregatedTransactions, -}: { - setup: Setup & SetupTimeRange; - bucketSize: string; - searchAggregatedTransactions: boolean; -}): Promise { - return withApmSpan( - 'observability_overview_get_transaction_distribution', - async () => { - const { apmEventClient, start, end } = setup; - - const { aggregations } = await apmEventClient.search({ - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: rangeQuery(start, end), - }, - }, - aggs: { - distribution: { - date_histogram: { - field: '@timestamp', - fixed_interval: bucketSize, - min_doc_count: 0, - }, - }, - }, - }, - }); - - return ( - aggregations?.distribution.buckets.map((bucket) => ({ - x: bucket.key, - y: calculateThroughput({ start, end, value: bucket.doc_count }), - })) || [] - ); - } - ); -} diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts new file mode 100644 index 0000000000000..da8ac7c50b594 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -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 { + TRANSACTION_PAGE_LOAD, + TRANSACTION_REQUEST, +} from '../../../common/transaction_types'; +import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; +import { rangeQuery } from '../../../server/utils/queries'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { calculateThroughput } from '../helpers/calculate_throughput'; +import { withApmSpan } from '../../utils/with_apm_span'; + +export function getTransactionsPerMinute({ + setup, + bucketSize, + searchAggregatedTransactions, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; + searchAggregatedTransactions: boolean; +}) { + return withApmSpan( + 'observability_overview_get_transactions_per_minute', + async () => { + const { apmEventClient, start, end } = setup; + + const { aggregations } = await apmEventClient.search({ + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + body: { + size: 0, + query: { + bool: { + filter: rangeQuery(start, end), + }, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, + }, + }, + }, + }, + }, + }); + + if (!aggregations || !aggregations.transactionType.buckets) { + return { value: undefined, timeseries: [] }; + } + + const topTransactionTypeBucket = + aggregations.transactionType.buckets.find( + ({ key: transactionType }) => + transactionType === TRANSACTION_REQUEST || + transactionType === TRANSACTION_PAGE_LOAD + ) || aggregations.transactionType.buckets[0]; + + return { + value: calculateThroughput({ + start, + end, + value: topTransactionTypeBucket?.doc_count || 0, + }), + timeseries: + topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.throughput.value, + })) || [], + }; + } + ); +} diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f556374179c51..db96794627519 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -16,6 +16,7 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; +import { SpacesPluginSetup } from '../../spaces/server'; import { APMConfig, APMXPackConfig } from '.'; import { mergeConfigs } from './index'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; @@ -65,6 +66,7 @@ export class APMPlugin implements Plugin { public setup( core: CoreSetup, plugins: { + spaces?: SpacesPluginSetup; apmOss: APMOSSPluginSetup; home: HomeServerPluginSetup; licensing: LicensingPluginSetup; @@ -148,11 +150,7 @@ export class APMPlugin implements Plugin { createApmApi().init(core, { config$: mergedConfig$, logger: this.logger!, - plugins: { - observability: plugins.observability, - security: plugins.security, - ml: plugins.ml, - }, + plugins, }); const boundGetApmIndices = async () => diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 48305d1a9df07..c7c69e0774822 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -9,8 +9,10 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; import * as t from 'io-ts'; import { isActivePlatinumLicense } from '../../common/license_check'; -import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_correlations_for_failed_transactions'; -import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions'; +import { getCorrelationsForFailedTransactions } from '../lib/correlations/errors/get_correlations_for_failed_transactions'; +import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overall_error_timeseries'; +import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions'; +import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution'; import { setupRequest } from '../lib/helpers/setup_request'; import { createRoute } from './create_route'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; @@ -23,8 +25,47 @@ const INVALID_LICENSE = i18n.translate( } ); +export const correlationsLatencyDistributionRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/latency/overall_distribution', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + const setup = await setupRequest(context, request); + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + } = context.params.query; + + return getOverallLatencyDistribution({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, + }); + }, +}); + export const correlationsForSlowTransactionsRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/slow_transactions', + endpoint: 'GET /api/apm/correlations/latency/slow_transactions', params: t.type({ query: t.intersection([ t.partial({ @@ -35,6 +76,8 @@ export const correlationsForSlowTransactionsRoute = createRoute({ t.type({ durationPercentile: t.string, fieldNames: t.string, + maxLatency: t.string, + distributionInterval: t.string, }), environmentRt, kueryRt, @@ -55,6 +98,8 @@ export const correlationsForSlowTransactionsRoute = createRoute({ transactionName, durationPercentile, fieldNames, + maxLatency, + distributionInterval, } = context.params.query; return getCorrelationsForSlowTransactions({ @@ -66,12 +111,53 @@ export const correlationsForSlowTransactionsRoute = createRoute({ durationPercentile: parseInt(durationPercentile, 10), fieldNames: fieldNames.split(','), setup, + maxLatency: parseInt(maxLatency, 10), + distributionInterval: parseInt(distributionInterval, 10), + }); + }, +}); + +export const correlationsErrorDistributionRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/errors/overall_timeseries', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } + const setup = await setupRequest(context, request); + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + } = context.params.query; + + return getOverallErrorTimeseries({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, }); }, }); export const correlationsForFailedTransactionsRoute = createRoute({ - endpoint: 'GET /api/apm/correlations/failed_transactions', + endpoint: 'GET /api/apm/correlations/errors/failed_transactions', params: t.type({ query: t.intersection([ t.partial({ diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 2b5fb0b516ab5..5b74aa4347f14 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -58,7 +58,9 @@ import { rootTransactionByTraceIdRoute, } from './traces'; import { + correlationsLatencyDistributionRoute, correlationsForSlowTransactionsRoute, + correlationsErrorDistributionRoute, correlationsForFailedTransactionsRoute, } from './correlations'; import { @@ -152,7 +154,9 @@ const createApmApi = () => { .add(createOrUpdateAgentConfigurationRoute) // Correlations + .add(correlationsLatencyDistributionRoute) .add(correlationsForSlowTransactionsRoute) + .add(correlationsErrorDistributionRoute) .add(correlationsForFailedTransactionsRoute) // APM indices diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index fd7d2120ab6f5..3b800c23135ce 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -21,10 +21,13 @@ export const staticIndexPatternRoute = createRoute((core) => ({ getInternalSavedObjectsClient(core), ]); + const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request); + const didCreateIndexPattern = await createStaticIndexPattern( setup, context, - savedObjectsClient + savedObjectsClient, + spaceId ); return { created: didCreateIndexPattern }; diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index b9c0a76b6fb90..1aac2c09d01c5 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; -import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates'; +import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute'; import { getHasData } from '../lib/observability_overview/has_data'; import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; @@ -39,18 +39,18 @@ export const observabilityOverviewRoute = createRoute({ ); return withApmSpan('observability_overview', async () => { - const [serviceCount, transactionCoordinates] = await Promise.all([ + const [serviceCount, transactionPerMinute] = await Promise.all([ getServiceCount({ setup, searchAggregatedTransactions, }), - getTransactionCoordinates({ + getTransactionsPerMinute({ setup, bucketSize, searchAggregatedTransactions, }), ]); - return { serviceCount, transactionCoordinates }; + return { serviceCount, transactionPerMinute }; }); }, }); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 1575041fb2f45..3ba24b4ed5268 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,6 +14,7 @@ import { } from 'src/core/server'; import { Observable } from 'rxjs'; import { RequiredKeys, DeepPartial } from 'utility-types'; +import { SpacesPluginStart } from '../../../spaces/server'; import { ObservabilityPluginSetup } from '../../../observability/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { SecurityPluginSetup } from '../../../security/server'; @@ -93,6 +94,7 @@ export type APMRequestHandlerContext< config: APMConfig; logger: Logger; plugins: { + spaces?: SpacesPluginStart; observability?: ObservabilityPluginSetup; security?: SecurityPluginSetup; ml?: MlPluginSetup; diff --git a/x-pack/plugins/banners/README.md b/x-pack/plugins/banners/README.md index 890c194e1bcb0..6ef2d9196f379 100644 --- a/x-pack/plugins/banners/README.md +++ b/x-pack/plugins/banners/README.md @@ -12,7 +12,7 @@ The options are The placement of the banner. The allowed values are: - `disabled` - The banner will be disabled - - `header` - The banner will be displayed in the header + - `top` - The banner will be displayed in the header - `textContent` @@ -31,7 +31,7 @@ The color for the banner's background. Must be a valid hex color `kibana.yml` ```yaml xpack.banners: - placement: 'header' + placement: 'top' textContent: 'Production environment - Proceed with **special levels** of caution' textColor: '#FF0000' backgroundColor: '#CC2211' diff --git a/x-pack/plugins/banners/common/types.ts b/x-pack/plugins/banners/common/types.ts index 0c785f516ddb3..6c16b4e8055bb 100644 --- a/x-pack/plugins/banners/common/types.ts +++ b/x-pack/plugins/banners/common/types.ts @@ -10,7 +10,7 @@ export interface BannerInfoResponse { banner: BannerConfiguration; } -export type BannerPlacement = 'disabled' | 'header'; +export type BannerPlacement = 'disabled' | 'top'; export interface BannerConfiguration { placement: BannerPlacement; diff --git a/x-pack/plugins/banners/public/components/banner.tsx b/x-pack/plugins/banners/public/components/banner.tsx index ea30e46881d0c..ae28986297659 100644 --- a/x-pack/plugins/banners/public/components/banner.tsx +++ b/x-pack/plugins/banners/public/components/banner.tsx @@ -26,7 +26,7 @@ export const Banner: FC = ({ bannerConfig }) => { }} >
- +
); diff --git a/x-pack/plugins/banners/public/plugin.test.tsx b/x-pack/plugins/banners/public/plugin.test.tsx index 036ad17e2598e..8722d9516b9db 100644 --- a/x-pack/plugins/banners/public/plugin.test.tsx +++ b/x-pack/plugins/banners/public/plugin.test.tsx @@ -7,11 +7,19 @@ import { getBannerInfoMock } from './plugin.test.mocks'; import { coreMock } from '../../../../src/core/public/mocks'; +import { BannerConfiguration } from '../common/types'; import { BannersPlugin } from './plugin'; -import { BannerClientConfig } from './types'; const nextTick = async () => await new Promise((resolve) => resolve()); +const createBannerConfig = (parts: Partial = {}): BannerConfiguration => ({ + placement: 'disabled', + textContent: 'foo', + textColor: '#FFFFFF', + backgroundColor: '#000000', + ...parts, +}); + describe('BannersPlugin', () => { let plugin: BannersPlugin; let pluginInitContext: ReturnType; @@ -25,11 +33,12 @@ describe('BannersPlugin', () => { getBannerInfoMock.mockResolvedValue({ allowed: false, + banner: createBannerConfig(), }); }); - const startPlugin = async (config: BannerClientConfig) => { - pluginInitContext = coreMock.createPluginInitializerContext(config); + const startPlugin = async () => { + pluginInitContext = coreMock.createPluginInitializerContext(); plugin = new BannersPlugin(pluginInitContext); plugin.setup(coreSetup); plugin.start(coreStart); @@ -41,46 +50,62 @@ describe('BannersPlugin', () => { getBannerInfoMock.mockReset(); }); - it('calls `getBannerInfo` if `config.placement !== disabled`', async () => { - await startPlugin({ - placement: 'header', + describe('when banner is allowed', () => { + it('registers the header banner if `banner.placement` is `top`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: true, + banner: createBannerConfig({ + placement: 'top', + }), + }); + + await startPlugin(); + + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(1); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledWith({ + content: expect.any(Function), + }); }); - expect(getBannerInfoMock).toHaveBeenCalledTimes(1); - }); + it('does not register the header banner if `banner.placement` is `disabled`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: true, + banner: createBannerConfig({ + placement: 'disabled', + }), + }); - it('does not call `getBannerInfo` if `config.placement === disabled`', async () => { - await startPlugin({ - placement: 'disabled', - }); + await startPlugin(); - expect(getBannerInfoMock).not.toHaveBeenCalled(); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(0); + }); }); - it('registers the header banner if `getBannerInfo` return `allowed=true`', async () => { - getBannerInfoMock.mockResolvedValue({ - allowed: true, - }); + describe('when banner is not allowed', () => { + it('does not register the header banner if `banner.placement` is `top`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: false, + banner: createBannerConfig({ + placement: 'top', + }), + }); - await startPlugin({ - placement: 'header', - }); + await startPlugin(); - expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(1); - expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledWith({ - content: expect.any(Function), + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(0); }); - }); - it('does not register the header banner if `getBannerInfo` return `allowed=false`', async () => { - getBannerInfoMock.mockResolvedValue({ - allowed: false, - }); + it('does not register the header banner if `banner.placement` is `disabled`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: false, + banner: createBannerConfig({ + placement: 'disabled', + }), + }); - await startPlugin({ - placement: 'header', - }); + await startPlugin(); - expect(coreStart.chrome.setHeaderBanner).not.toHaveBeenCalled(); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/x-pack/plugins/banners/public/plugin.tsx b/x-pack/plugins/banners/public/plugin.tsx index dca99a816a25b..014d2de58b9ea 100644 --- a/x-pack/plugins/banners/public/plugin.tsx +++ b/x-pack/plugins/banners/public/plugin.tsx @@ -9,35 +9,28 @@ import React from 'react'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; import { Banner } from './components'; -import { BannerClientConfig } from './types'; import { getBannerInfo } from './get_banner_info'; export class BannersPlugin implements Plugin<{}, {}, {}, {}> { - private readonly config: BannerClientConfig; - - constructor(context: PluginInitializerContext) { - this.config = context.config.get(); - } + constructor(context: PluginInitializerContext) {} setup({}: CoreSetup<{}, {}>) { return {}; } start({ chrome, uiSettings, http }: CoreStart) { - if (this.config.placement !== 'disabled') { - getBannerInfo(http).then( - ({ allowed, banner }) => { - if (allowed) { - chrome.setHeaderBanner({ - content: toMountPoint(), - }); - } - }, - () => { - chrome.setHeaderBanner(undefined); + getBannerInfo(http).then( + ({ allowed, banner }) => { + if (allowed && banner.placement === 'top') { + chrome.setHeaderBanner({ + content: toMountPoint(), + }); } - ); - } + }, + () => { + chrome.setHeaderBanner(undefined); + } + ); return {}; } diff --git a/x-pack/plugins/banners/server/config.ts b/x-pack/plugins/banners/server/config.ts index 9a8cc9680c296..ec1c7006a84ce 100644 --- a/x-pack/plugins/banners/server/config.ts +++ b/x-pack/plugins/banners/server/config.ts @@ -5,12 +5,13 @@ * 2.0. */ +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from 'kibana/server'; import { isHexColor } from './utils'; const configSchema = schema.object({ - placement: schema.oneOf([schema.literal('disabled'), schema.literal('header')], { + placement: schema.oneOf([schema.literal('disabled'), schema.literal('top')], { defaultValue: 'disabled', }), textContent: schema.string({ defaultValue: '' }), @@ -30,13 +31,25 @@ const configSchema = schema.object({ }, defaultValue: '#FFF9E8', }), + disableSpaceBanners: schema.boolean({ defaultValue: false }), }); export type BannersConfigType = TypeOf; export const config: PluginConfigDescriptor = { schema: configSchema, - exposeToBrowser: { - placement: true, - }, + exposeToBrowser: {}, + deprecations: () => [ + (rootConfig, fromPath, addDeprecation) => { + const pluginConfig = get(rootConfig, fromPath); + if (pluginConfig?.placement === 'header') { + addDeprecation({ + message: 'The `header` value for xpack.banners.placement has been replaced by `top`', + }); + pluginConfig.placement = 'top'; + } + + return rootConfig; + }, + ], }; diff --git a/x-pack/plugins/banners/server/plugin.test.mocks.ts b/x-pack/plugins/banners/server/plugin.test.mocks.ts new file mode 100644 index 0000000000000..316699c2c2020 --- /dev/null +++ b/x-pack/plugins/banners/server/plugin.test.mocks.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 registerRoutesMock = jest.fn(); +jest.doMock('./routes', () => ({ + registerRoutes: registerRoutesMock, +})); + +export const registerSettingsMock = jest.fn(); +jest.doMock('./ui_settings', () => ({ + registerSettings: registerSettingsMock, +})); diff --git a/x-pack/plugins/banners/server/plugin.test.ts b/x-pack/plugins/banners/server/plugin.test.ts new file mode 100644 index 0000000000000..b3f8c7be696cd --- /dev/null +++ b/x-pack/plugins/banners/server/plugin.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerRoutesMock, registerSettingsMock } from './plugin.test.mocks'; + +import { coreMock } from '../../../../src/core/server/mocks'; +import { BannersPlugin } from './plugin'; +import { BannersConfigType } from './config'; + +describe('BannersPlugin', () => { + let plugin: BannersPlugin; + let pluginInitContext: ReturnType; + let coreSetup: ReturnType; + let bannerConfig: BannersConfigType; + + beforeEach(() => { + bannerConfig = { + placement: 'top', + textContent: 'foo', + backgroundColor: '#000000', + textColor: '#FFFFFF', + disableSpaceBanners: false, + }; + pluginInitContext = coreMock.createPluginInitializerContext(); + pluginInitContext.config.get.mockReturnValue(bannerConfig); + coreSetup = coreMock.createSetup(); + + plugin = new BannersPlugin(pluginInitContext); + }); + + afterEach(() => { + registerRoutesMock.mockReset(); + registerSettingsMock.mockReset(); + }); + + describe('#setup', () => { + it('calls `registerRoutes` with the correct parameters', () => { + plugin.setup(coreSetup); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith(expect.any(Object), bannerConfig); + }); + it('calls `registerSettings` with the correct parameters', () => { + plugin.setup(coreSetup); + + expect(registerSettingsMock).toHaveBeenCalledTimes(1); + expect(registerSettingsMock).toHaveBeenCalledWith(coreSetup.uiSettings, bannerConfig); + }); + }); +}); diff --git a/x-pack/plugins/banners/server/plugin.ts b/x-pack/plugins/banners/server/plugin.ts index 66cd083189975..852ba135c478b 100644 --- a/x-pack/plugins/banners/server/plugin.ts +++ b/x-pack/plugins/banners/server/plugin.ts @@ -6,21 +6,22 @@ */ import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; -import { BannerConfiguration } from '../common'; import { BannersConfigType } from './config'; import { BannersRequestHandlerContext } from './types'; import { registerRoutes } from './routes'; +import { registerSettings } from './ui_settings'; export class BannersPlugin implements Plugin<{}, {}, {}, {}> { - private readonly config: BannerConfiguration; + private readonly config: BannersConfigType; constructor(context: PluginInitializerContext) { - this.config = convertConfig(context.config.get()); + this.config = context.config.get(); } setup({ uiSettings, getStartServices, http }: CoreSetup<{}, {}>) { const router = http.createRouter(); registerRoutes(router, this.config); + registerSettings(uiSettings, this.config); return {}; } @@ -29,5 +30,3 @@ export class BannersPlugin implements Plugin<{}, {}, {}, {}> { return {}; } } - -const convertConfig = (raw: BannersConfigType): BannerConfiguration => raw; diff --git a/x-pack/plugins/banners/server/routes/index.ts b/x-pack/plugins/banners/server/routes/index.ts index a4eedc3234c86..347236b4df4e2 100644 --- a/x-pack/plugins/banners/server/routes/index.ts +++ b/x-pack/plugins/banners/server/routes/index.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { BannerConfiguration } from '../../common'; +import { BannersConfigType } from '../config'; import { BannersRouter } from '../types'; import { registerInfoRoute } from './info'; -export const registerRoutes = (router: BannersRouter, config: BannerConfiguration) => { +export const registerRoutes = (router: BannersRouter, config: BannersConfigType) => { registerInfoRoute(router, config); }; diff --git a/x-pack/plugins/banners/server/routes/info.ts b/x-pack/plugins/banners/server/routes/info.ts index e0db842028c37..806c4a7ae6b39 100644 --- a/x-pack/plugins/banners/server/routes/info.ts +++ b/x-pack/plugins/banners/server/routes/info.ts @@ -5,26 +5,33 @@ * 2.0. */ +import { IUiSettingsClient } from 'kibana/server'; import { ILicense } from '../../../licensing/server'; -import { BannerInfoResponse, BannerConfiguration } from '../../common'; +import { BannersConfigType } from '../config'; +import { BannerInfoResponse, BannerConfiguration, BannerPlacement } from '../../common'; import { BannersRouter } from '../types'; -export const registerInfoRoute = (router: BannersRouter, config: BannerConfiguration) => { +export const registerInfoRoute = (router: BannersRouter, config: BannersConfigType) => { router.get( { path: '/api/banners/info', validate: false, options: { - authRequired: false, + authRequired: 'optional', }, }, - (ctx, req, res) => { + async (ctx, req, res) => { const allowed = isValidLicense(ctx.licensing.license); + const bannerConfig = + req.auth.isAuthenticated && config.disableSpaceBanners === false + ? await getBannerConfig(ctx.core.uiSettings.client) + : config; + return res.ok({ body: { allowed, - banner: config, + banner: bannerConfig, } as BannerInfoResponse, }); } @@ -34,3 +41,19 @@ export const registerInfoRoute = (router: BannersRouter, config: BannerConfigura const isValidLicense = (license: ILicense): boolean => { return license.hasAtLeast('gold'); }; + +const getBannerConfig = async (client: IUiSettingsClient): Promise => { + const [placement, textContent, textColor, backgroundColor] = await Promise.all([ + client.get('banners:placement'), + client.get('banners:textContent'), + client.get('banners:textColor'), + client.get('banners:backgroundColor'), + ]); + + return { + placement, + textContent, + textColor, + backgroundColor, + }; +}; diff --git a/x-pack/plugins/banners/server/ui_settings.test.ts b/x-pack/plugins/banners/server/ui_settings.test.ts new file mode 100644 index 0000000000000..9fae019774336 --- /dev/null +++ b/x-pack/plugins/banners/server/ui_settings.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { uiSettingsServiceMock } from '../../../../src/core/server/mocks'; +import { BannersConfigType } from './config'; +import { registerSettings } from './ui_settings'; + +const createConfig = (parts: Partial = {}): BannersConfigType => ({ + placement: 'disabled', + backgroundColor: '#0000', + textColor: '#FFFFFF', + textContent: 'Hello from the banner', + disableSpaceBanners: false, + ...parts, +}); + +describe('registerSettings', () => { + let uiSettings: ReturnType; + + beforeEach(() => { + uiSettings = uiSettingsServiceMock.createSetupContract(); + }); + + it('registers the settings', () => { + registerSettings(uiSettings, createConfig()); + + expect(uiSettings.register).toHaveBeenCalledTimes(1); + expect(uiSettings.register).toHaveBeenCalledWith({ + 'banners:placement': expect.any(Object), + 'banners:textContent': expect.any(Object), + 'banners:textColor': expect.any(Object), + 'banners:backgroundColor': expect.any(Object), + }); + }); + + it('does not register the settings if `config.disableSpaceBanners` is `true`', () => { + registerSettings(uiSettings, createConfig({ disableSpaceBanners: true })); + + expect(uiSettings.register).not.toHaveBeenCalled(); + }); + + it('uses the configuration values as defaults', () => { + const config = createConfig({ + placement: 'top', + backgroundColor: '#FF00CC', + textColor: '#AAFFEE', + textContent: 'Some text', + }); + + registerSettings(uiSettings, config); + + expect(uiSettings.register).toHaveBeenCalledTimes(1); + expect(uiSettings.register).toHaveBeenCalledWith({ + 'banners:placement': expect.objectContaining({ + value: config.placement, + }), + 'banners:textContent': expect.objectContaining({ + value: config.textContent, + }), + 'banners:textColor': expect.objectContaining({ + value: config.textColor, + }), + 'banners:backgroundColor': expect.objectContaining({ + value: config.backgroundColor, + }), + }); + }); +}); diff --git a/x-pack/plugins/banners/server/ui_settings.ts b/x-pack/plugins/banners/server/ui_settings.ts new file mode 100644 index 0000000000000..d35ab76c41a58 --- /dev/null +++ b/x-pack/plugins/banners/server/ui_settings.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsServiceSetup } from 'src/core/server'; +import { BannersConfigType } from './config'; +import { isHexColor } from './utils'; + +export const registerSettings = (uiSettings: UiSettingsServiceSetup, config: BannersConfigType) => { + if (config.disableSpaceBanners) { + return; + } + + const subscriptionLink = ` + + ${i18n.translate('xpack.banners.settings.subscriptionRequiredLink.text', { + defaultMessage: 'Subscription required.', + })} + + `; + + uiSettings.register({ + 'banners:placement': { + name: i18n.translate('xpack.banners.settings.placement.title', { + defaultMessage: 'Banner placement', + }), + description: i18n.translate('xpack.banners.settings.placement.description', { + defaultMessage: + 'Display a top banner for this space, above the Elastic header. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + category: ['banner'], + order: 1, + type: 'select', + value: config.placement, + options: ['disabled', 'top'], + optionLabels: { + disabled: i18n.translate('xpack.banners.settings.placement.disabled', { + defaultMessage: 'Disabled', + }), + top: i18n.translate('xpack.banners.settings.placement.top', { + defaultMessage: 'Top', + }), + }, + requiresPageReload: true, + schema: schema.oneOf([schema.literal('disabled'), schema.literal('top')]), + }, + 'banners:textContent': { + name: i18n.translate('xpack.banners.settings.textContent.title', { + defaultMessage: 'Banner text', + }), + description: i18n.translate('xpack.banners.settings.text.description', { + defaultMessage: 'Add Markdown-formatted text to the banner. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + sensitive: true, + category: ['banner'], + order: 2, + type: 'markdown', + value: config.textContent, + requiresPageReload: true, + schema: schema.string(), + }, + 'banners:textColor': { + name: i18n.translate('xpack.banners.settings.textColor.title', { + defaultMessage: 'Banner text color', + }), + description: i18n.translate('xpack.banners.settings.textColor.description', { + defaultMessage: 'Set the color of the banner text. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + category: ['banner'], + order: 3, + type: 'color', + value: config.textColor, + requiresPageReload: true, + schema: schema.string({ + validate: (color) => { + if (!isHexColor(color)) { + return `'banners:textColor' must be an hex color`; + } + }, + }), + }, + 'banners:backgroundColor': { + name: i18n.translate('xpack.banners.settings.backgroundColor.title', { + defaultMessage: 'Banner background color', + }), + description: i18n.translate('xpack.banners.settings.backgroundColor.description', { + defaultMessage: 'Set the background color for the banner. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + category: ['banner'], + order: 4, + type: 'color', + value: config.backgroundColor, + requiresPageReload: true, + schema: schema.string({ + validate: (color) => { + if (!isHexColor(color)) { + return `'banners:backgroundColor' must be an hex color`; + } + }, + }), + }, + }); +}; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss index 2ed6884542b18..620e0eb113d36 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.scss @@ -66,7 +66,7 @@ text-decoration: none; .canvasPageManager__pagePreview { - @include euiBottomShadowMedium($opacity: .3); + @include euiBottomShadowMedium; } .canvasPageManager__controls { diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss index 9266273406b84..e770f10927552 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_page.scss @@ -1,5 +1,5 @@ .canvasPage { - @include euiBottomShadowFlat($opacity: .4); + @include euiBottomShadowFlat; z-index: initial; position: absolute; top: 0; diff --git a/x-pack/plugins/canvas/scripts/shareable_runtime.js b/x-pack/plugins/canvas/scripts/shareable_runtime.js index 7f7f6d235c984..b760a92811b8e 100644 --- a/x-pack/plugins/canvas/scripts/shareable_runtime.js +++ b/x-pack/plugins/canvas/scripts/shareable_runtime.js @@ -56,7 +56,7 @@ run( 'webpack-dev-server', '--config', webpackConfig, - ...(process.stdout.isTTY ? ['--progress'] : []), + ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), '--hide-modules', '--display-entrypoints', 'false', @@ -93,7 +93,7 @@ run( '--config', webpackConfig, '--hide-modules', - ...(process.stdout.isTTY ? ['--progress'] : []), + ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), ], { ...options, diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index 88af1cf6d38bb..e6b8a66b9026f 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -44,7 +44,7 @@ run( 'webpack', '--config', 'x-pack/plugins/canvas/storybook/webpack.dll.config.js', - '--progress', + ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), '--hide-modules', '--display-entrypoints', 'false', diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index acf71cad3f3ba..b68642d184542 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,7 +59,8 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/95899 +describe.skip('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts index 8d9941073140f..52a027e899d0d 100644 --- a/x-pack/plugins/cloud/public/mocks.ts +++ b/x-pack/plugins/cloud/public/mocks.ts @@ -9,8 +9,11 @@ function createSetupMock() { return { cloudId: 'mock-cloud-id', isCloudEnabled: true, - resetPasswordUrl: 'reset-password-url', - accountUrl: 'account-url', + cname: 'cname', + baseUrl: 'base-url', + deploymentUrl: 'deployment-url', + profileUrl: 'profile-url', + organizationUrl: 'organization-url', }; } diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 4c12aa3d92b47..8ca4f7711811a 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -12,12 +12,15 @@ import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { createUserMenuLinks } from './user_menu_links'; +import { getFullCloudUrl } from './utils'; export interface CloudConfigType { id?: string; - resetPasswordUrl?: string; - deploymentUrl?: string; - accountUrl?: string; + cname?: string; + base_url?: string; + profile_url?: string; + deployment_url?: string; + organization_url?: string; } interface CloudSetupDependencies { @@ -30,10 +33,12 @@ interface CloudStartDependencies { export interface CloudSetup { cloudId?: string; - cloudDeploymentUrl?: string; + cname?: string; + baseUrl?: string; + deploymentUrl?: string; + profileUrl?: string; + organizationUrl?: string; isCloudEnabled: boolean; - resetPasswordUrl?: string; - accountUrl?: string; } export class CloudPlugin implements Plugin { @@ -46,33 +51,44 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id, resetPasswordUrl, deploymentUrl } = this.config; + const { + id, + cname, + profile_url: profileUrl, + organization_url: organizationUrl, + deployment_url: deploymentUrl, + base_url: baseUrl, + } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); if (home) { home.environment.update({ cloud: this.isCloudEnabled }); if (this.isCloudEnabled) { - home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); + home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl }); } } return { cloudId: id, - cloudDeploymentUrl: deploymentUrl, + cname, + baseUrl, + deploymentUrl: getFullCloudUrl(baseUrl, deploymentUrl), + profileUrl: getFullCloudUrl(baseUrl, profileUrl), + organizationUrl: getFullCloudUrl(baseUrl, organizationUrl), isCloudEnabled: this.isCloudEnabled, }; } public start(coreStart: CoreStart, { security }: CloudStartDependencies) { - const { deploymentUrl } = this.config; + const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); - if (deploymentUrl) { + if (baseUrl && deploymentUrl) { coreStart.chrome.setCustomNavLink({ title: i18n.translate('xpack.cloud.deploymentLinkLabel', { defaultMessage: 'Manage this deployment', }), euiIconType: 'arrowLeft', - href: deploymentUrl, + href: getFullCloudUrl(baseUrl, deploymentUrl), }); } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts index e662d51500333..f00911d577c59 100644 --- a/x-pack/plugins/cloud/public/user_menu_links.ts +++ b/x-pack/plugins/cloud/public/user_menu_links.ts @@ -8,30 +8,31 @@ import { i18n } from '@kbn/i18n'; import { UserMenuLink } from '../../security/public'; import { CloudConfigType } from '.'; +import { getFullCloudUrl } from './utils'; export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => { - const { resetPasswordUrl, accountUrl } = config; + const { profile_url: profileUrl, organization_url: organizationUrl, base_url: baseUrl } = config; const userMenuLinks = [] as UserMenuLink[]; - if (resetPasswordUrl) { + if (baseUrl && profileUrl) { userMenuLinks.push({ label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { defaultMessage: 'Profile', }), iconType: 'user', - href: resetPasswordUrl, + href: getFullCloudUrl(baseUrl, profileUrl), order: 100, setAsProfile: true, }); } - if (accountUrl) { + if (baseUrl && organizationUrl) { userMenuLinks.push({ label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', { defaultMessage: 'Account & Billing', }), iconType: 'gear', - href: accountUrl, + href: getFullCloudUrl(baseUrl, organizationUrl), order: 200, }); } diff --git a/x-pack/plugins/cloud/public/utils.ts b/x-pack/plugins/cloud/public/utils.ts new file mode 100644 index 0000000000000..e4d4ace64563c --- /dev/null +++ b/x-pack/plugins/cloud/public/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getFullCloudUrl(baseUrl: string | undefined, dirPath: string | undefined) { + if (baseUrl && dirPath) { + return `${baseUrl}${dirPath}`; + } + + return ''; +} diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 673df5ac2203b..0e73d59667131 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -22,9 +22,11 @@ const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), id: schema.maybe(schema.string()), apm: schema.maybe(apmConfigSchema), - resetPasswordUrl: schema.maybe(schema.string()), - deploymentUrl: schema.maybe(schema.string()), - accountUrl: schema.maybe(schema.string()), + cname: schema.maybe(schema.string()), + base_url: schema.maybe(schema.string()), + profile_url: schema.maybe(schema.string()), + deployment_url: schema.maybe(schema.string()), + organization_url: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -32,9 +34,11 @@ export type CloudConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { id: true, - resetPasswordUrl: true, - deploymentUrl: true, - accountUrl: true, + cname: true, + base_url: true, + profile_url: true, + deployment_url: true, + organization_url: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts index f9c62069154b6..2611f6c9da19f 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts @@ -13,9 +13,13 @@ import { EQL_SEARCH_STRATEGY, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import type { SavedObjectsClientContract } from 'kibana/server'; import { SearchSessionsConfig, SearchStatus } from './types'; import moment from 'moment'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsDeleteOptions, + SavedObjectsClientContract, +} from '../../../../../../src/core/server'; describe('getSearchStatus', () => { let mockClient: any; @@ -263,6 +267,45 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); + test('deletes in space', async () => { + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { + id: '123', + namespaces: ['awesome'], + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(2, 'm')), + idMapping: { + 'map-key': { + strategy: ENHANCED_ES_SEARCH_STRATEGY, + id: 'async-id', + }, + }, + }, + }, + ], + total: 1, + } as any); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(savedObjectsClient.delete).toBeCalled(); + + const [, id, opts] = savedObjectsClient.delete.mock.calls[0]; + expect(id).toBe('123'); + expect((opts as SavedObjectsDeleteOptions).namespace).toBe('awesome'); + }); + test('deletes a non persisted, abandoned session', async () => { savedObjectsClient.find.mockResolvedValue({ saved_objects: [ @@ -479,6 +522,50 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); + test('updates in space', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + namespaces: ['awesome'], + attributes: { + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + }; + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [so], + total: 1, + } as any); + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + + await checkRunningSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' }); + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + const updatedAttributes = updateInput[0] as SavedObjectsBulkUpdateObject; + expect(updatedAttributes.namespace).toBe('awesome'); + }); + test('updates to complete if the search is done', async () => { savedObjectsClient.bulkUpdate = jest.fn(); const so = { @@ -563,7 +650,6 @@ describe('getSearchStatus', () => { config ); const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; - const updatedAttributes = updateInput[0].attributes as SearchSessionSavedObjectAttributes; expect(updatedAttributes.status).toBe(SearchSessionStatus.ERROR); expect(updatedAttributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index e521c39d7cfd3..6e52b17f36803 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -10,6 +10,7 @@ import { Logger, SavedObjectsClientContract, SavedObjectsFindResult, + SavedObjectsUpdateResponse, } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; @@ -169,12 +170,20 @@ export async function checkRunningSessions( if (!session.attributes.persisted) { if (isSessionStale(session, config, logger)) { - deleted = true; // delete saved object to free up memory // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! // Maybe we want to change state to deleted and cleanup later? logger.debug(`Deleting stale session | ${session.id}`); - await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id); + try { + await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { + namespace: session.namespaces?.[0], + }); + deleted = true; + } catch (e) { + logger.error( + `Error while deleting stale search session ${session.id}: ${e.message}` + ); + } // Send a delete request for each async search to ES Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { @@ -183,8 +192,8 @@ export async function checkRunningSessions( try { await client.asyncSearch.delete({ id: searchInfo.id }); } catch (e) { - logger.debug( - `Error ignored while deleting async_search ${searchInfo.id}: ${e.message}` + logger.error( + `Error while deleting async_search ${searchInfo.id}: ${e.message}` ); } } @@ -202,9 +211,31 @@ export async function checkRunningSessions( if (updatedSessions.length) { // If there's an error, we'll try again in the next iteration, so there's no need to check the output. const updatedResponse = await savedObjectsClient.bulkUpdate( - updatedSessions + updatedSessions.map((session) => ({ + ...session, + namespace: session.namespaces?.[0], + })) + ); + + const success: Array< + SavedObjectsUpdateResponse + > = []; + const fail: Array> = []; + + updatedResponse.saved_objects.forEach((savedObjectResponse) => { + if ('error' in savedObjectResponse) { + fail.push(savedObjectResponse); + logger.error( + `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` + ); + } else { + success.push(savedObjectResponse); + } + }); + + logger.debug( + `Updating search sessions: success: ${success.length}, fail: ${fail.length}` ); - logger.debug(`Updated ${updatedResponse.saved_objects.length} search sessions`); } }) ) diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts index ac2f4ba50d7f9..17e22e6f23daf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts @@ -24,6 +24,8 @@ export const mockFlashMessageHelpers = { setQueuedSuccessMessage: jest.fn(), setQueuedErrorMessage: jest.fn(), clearFlashMessages: jest.fn(), + flashSuccessToast: jest.fn(), + flashErrorToast: jest.fn(), }; jest.mock('../shared/flash_messages', () => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index d8d66e5ee1998..2325ddcf2b270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -14,11 +14,12 @@ export const mockKibanaValues = { charts: chartPluginMock.createStartContract(), cloud: { isCloudEnabled: false, - cloudDeploymentUrl: 'https://cloud.elastic.co/deployments/some-id', + deployment_url: 'https://cloud.elastic.co/deployments/some-id', }, history: mockHistory, navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), + setChromeIsVisible: jest.fn(), setDocTitle: jest.fn(), renderHeaderActions: jest.fn(), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx index 82b0d9a318f1d..0eef9b0c688c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx @@ -83,8 +83,7 @@ export const Analytics: React.FC = () => { /> - {/* TODO: Update this panel to use the bordered version once available */} - + { {RECENT_QUERIES}} subtitle={i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.recentQueriesDescription', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx index f00c4e29a7190..83c83aa36f1bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useValues } from 'kea'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; @@ -56,17 +56,19 @@ export const QueryDetail: React.FC = ({ breadcrumbs }) => { /> - + + + { + const values = { + isFlyoutOpen: true, + apiLog: mockApiLog, + }; + const actions = { + closeFlyout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('h2').text()).toEqual('Request details'); + expect(wrapper.find(ApiLogHeading).last().dive().find('h3').text()).toEqual('Response body'); + expect(wrapper.find(EuiBadge).prop('children')).toEqual('POST'); + }); + + it('closes the flyout', () => { + const wrapper = shallow(); + + wrapper.find(EuiFlyout).simulate('close'); + expect(actions.closeFlyout).toHaveBeenCalled(); + }); + + it('does not render if the flyout is not open', () => { + setMockValues({ ...values, isFlyoutOpen: false }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('does not render if a current apiLog has not been set', () => { + setMockValues({ ...values, apiLog: null }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx new file mode 100644 index 0000000000000..dd53e997da0f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_flyout.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.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 { useActions, useValues } from 'kea'; + +import { + EuiPortal, + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiHealth, + EuiText, + EuiCode, + EuiCodeBlock, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { getStatusColor, attemptToFormatJson } from '../utils'; + +import { ApiLogLogic } from './'; + +export const ApiLogFlyout: React.FC = () => { + const { isFlyoutOpen, apiLog } = useValues(ApiLogLogic); + const { closeFlyout } = useActions(ApiLogLogic); + + if (!isFlyoutOpen) return null; + if (!apiLog) return null; + + return ( + + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.flyout.title', { + defaultMessage: 'Request details', + })} +

+
+
+ + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTitle', { + defaultMessage: 'Method', + })} + +
+ {apiLog.http_method} +
+
+ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTitle', { + defaultMessage: 'Status', + })} + + {apiLog.status} + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timestampTitle', { + defaultMessage: 'Timestamp', + })} + + {apiLog.timestamp} + +
+ + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.userAgentTitle', { + defaultMessage: 'User agent', + })} + + + {apiLog.user_agent} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestPathTitle', { + defaultMessage: 'Request path', + })} + + + {apiLog.full_request_path} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.requestBodyTitle', { + defaultMessage: 'Request body', + })} + + + {attemptToFormatJson(apiLog.request_body)} + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.responseBodyTitle', { + defaultMessage: 'Response body', + })} + + + {attemptToFormatJson(apiLog.response_body)} + +
+
+
+ ); +}; + +export const ApiLogHeading: React.FC = ({ children }) => ( + +

{children}

+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx new file mode 100644 index 0000000000000..2b7ca7510e8e1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.test.tsx @@ -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 { LogicMounter } from '../../../../__mocks__'; +import { mockApiLog } from '../__mocks__/api_log.mock'; + +import { ApiLogLogic } from './'; + +describe('ApiLogLogic', () => { + const { mount } = new LogicMounter(ApiLogLogic); + + const DEFAULT_VALUES = { + isFlyoutOpen: false, + apiLog: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ApiLogLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('openFlyout', () => { + it('sets isFlyoutOpen to true & sets the current apiLog', () => { + mount({ isFlyoutOpen: false, apiLog: null }); + ApiLogLogic.actions.openFlyout(mockApiLog); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: true, + apiLog: mockApiLog, + }); + }); + }); + + describe('closeFlyout', () => { + it('sets isFlyoutOpen to false & resets the current apiLog', () => { + mount({ isFlyoutOpen: true, apiLog: mockApiLog }); + ApiLogLogic.actions.closeFlyout(); + + expect(ApiLogLogic.values).toEqual({ + ...DEFAULT_VALUES, + isFlyoutOpen: false, + apiLog: null, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts new file mode 100644 index 0000000000000..8b7c5f70f605c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/api_log_logic.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { ApiLog } from '../types'; + +interface ApiLogValues { + isFlyoutOpen: boolean; + apiLog: ApiLog | null; +} + +interface ApiLogActions { + openFlyout(apiLog: ApiLog): { apiLog: ApiLog }; + closeFlyout(): void; +} + +export const ApiLogLogic = kea>({ + path: ['enterprise_search', 'app_search', 'api_log_logic'], + actions: () => ({ + openFlyout: (apiLog) => ({ apiLog }), + closeFlyout: true, + }), + reducers: () => ({ + isFlyoutOpen: [ + false, + { + openFlyout: () => true, + closeFlyout: () => false, + }, + ], + apiLog: [ + null, + { + openFlyout: (_, { apiLog }) => apiLog, + closeFlyout: () => null, + }, + ], + }), +}); diff --git a/x-pack/plugins/maps_legacy_licensing/public/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts similarity index 69% rename from x-pack/plugins/maps_legacy_licensing/public/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts index 9105919eaa635..dcf949d9bf222 100644 --- a/x-pack/plugins/maps_legacy_licensing/public/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_log/index.ts @@ -5,8 +5,5 @@ * 2.0. */ -import { MapsLegacyLicensing } from './plugin'; - -export function plugin() { - return new MapsLegacyLicensing(); -} +export { ApiLogFlyout } from './api_log_flyout'; +export { ApiLogLogic } from './api_log_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx index da57fd466ffe1..1945dde84ec45 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.test.tsx @@ -5,28 +5,70 @@ * 2.0. */ +import { setMockValues, setMockActions, rerender } from '../../../__mocks__'; +import '../../../__mocks__/shallow_useeffect.mock'; + import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { EuiPageHeader } from '@elastic/eui'; +import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention'; +import { ApiLogsTable, NewApiEventsPrompt } from './components'; + import { ApiLogs } from './'; describe('ApiLogs', () => { + const values = { + dataLoading: false, + apiLogs: [], + meta: { page: { current: 1 } }, + }; + const actions = { + fetchApiLogs: jest.fn(), + pollForApiLogs: jest.fn(), + }; + + let wrapper: ShallowWrapper; + beforeEach(() => { jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + wrapper = shallow(); }); it('renders', () => { - const wrapper = shallow(); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs'); - // TODO: Check for ApiLogsTable + NewApiEventsPrompt when those get added + expect(wrapper.find(ApiLogsTable)).toHaveLength(1); + expect(wrapper.find(NewApiEventsPrompt)).toHaveLength(1); expect(wrapper.find(LogRetentionCallout).prop('type')).toEqual('api'); expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api'); }); + + it('renders a loading screen', () => { + setMockValues({ ...values, dataLoading: true, apiLogs: [] }); + rerender(wrapper); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + describe('effects', () => { + it('calls a manual fetchApiLogs on page load and pagination', () => { + expect(actions.fetchApiLogs).toHaveBeenCalledTimes(1); + + setMockValues({ ...values, meta: { page: { current: 2 } } }); + rerender(wrapper); + + expect(actions.fetchApiLogs).toHaveBeenCalledTimes(2); + }); + + it('starts pollForApiLogs on page load', () => { + expect(actions.pollForApiLogs).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx index 7e3fadb44fc7a..4690911fad772 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs.tsx @@ -5,22 +5,50 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; -import { EuiPageHeader, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useValues, useActions } from 'kea'; + +import { + EuiPageHeader, + EuiTitle, + EuiPageContent, + EuiPageContentBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs'; +import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention'; +import { ApiLogFlyout } from './api_log'; +import { ApiLogsTable, NewApiEventsPrompt } from './components'; import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants'; +import { ApiLogsLogic } from './'; + interface Props { engineBreadcrumb: BreadcrumbTrail; } export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { + const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic); + const { fetchApiLogs, pollForApiLogs } = useActions(ApiLogsLogic); + + useEffect(() => { + fetchApiLogs(); + }, [meta.page.current]); + + useEffect(() => { + pollForApiLogs(); + }, []); + + if (dataLoading && !apiLogs.length) return ; + return ( <> @@ -29,19 +57,28 @@ export const ApiLogs: React.FC = ({ engineBreadcrumb }) => { - - - -

{RECENT_API_EVENTS}

-
-
- - - - {/* TODO: NewApiEventsPrompt */} -
- - {/* TODO: ApiLogsTable */} + + + + + +

{RECENT_API_EVENTS}

+
+
+ + + + + + + +
+ + + + +
+
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts new file mode 100644 index 0000000000000..2eda4c6323fa5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts @@ -0,0 +1,300 @@ +/* + * Copyright 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 { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { mockApiLog } from './__mocks__/api_log.mock'; +import '../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { DEFAULT_META } from '../../../shared/constants'; + +import { ApiLogsLogic } from './'; + +describe('ApiLogsLogic', () => { + const { mount, unmount } = new LogicMounter(ApiLogsLogic); + const { http } = mockHttpValues; + const { flashAPIErrors, flashErrorToast } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + dataLoading: true, + apiLogs: [], + meta: DEFAULT_META, + hasNewData: false, + polledData: {}, + intervalId: null, + }; + + const MOCK_API_RESPONSE = { + results: [mockApiLog, mockApiLog], + meta: { + page: { + current: 1, + total_pages: 10, + total_results: 100, + size: 10, + }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ApiLogsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onPollStart', () => { + it('sets intervalId state', () => { + mount(); + ApiLogsLogic.actions.onPollStart(123); + + expect(ApiLogsLogic.values).toEqual({ + ...DEFAULT_VALUES, + intervalId: 123, + }); + }); + }); + + describe('storePolledData', () => { + it('sets hasNewData to true & polledData state', () => { + mount({ hasNewData: false }); + ApiLogsLogic.actions.storePolledData(MOCK_API_RESPONSE); + + expect(ApiLogsLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasNewData: true, + polledData: MOCK_API_RESPONSE, + }); + }); + }); + + describe('updateView', () => { + it('sets dataLoading & hasNewData to false, sets apiLogs & meta state', () => { + mount({ dataLoading: true, hasNewData: true }); + ApiLogsLogic.actions.updateView(MOCK_API_RESPONSE); + + expect(ApiLogsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + hasNewData: false, + apiLogs: MOCK_API_RESPONSE.results, + meta: MOCK_API_RESPONSE.meta, + }); + }); + }); + + describe('onPaginate', () => { + it('sets dataLoading to true & sets meta state', () => { + mount({ dataLoading: false }); + ApiLogsLogic.actions.onPaginate(5); + + expect(ApiLogsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + meta: { + ...DEFAULT_META, + page: { + ...DEFAULT_META.page, + current: 5, + }, + }, + }); + }); + }); + }); + + describe('listeners', () => { + describe('pollForApiLogs', () => { + jest.useFakeTimers(); + const setIntervalSpy = jest.spyOn(global, 'setInterval'); + + it('starts a poll that calls fetchApiLogs at set intervals', () => { + mount(); + jest.spyOn(ApiLogsLogic.actions, 'onPollStart'); + jest.spyOn(ApiLogsLogic.actions, 'fetchApiLogs'); + + ApiLogsLogic.actions.pollForApiLogs(); + expect(setIntervalSpy).toHaveBeenCalled(); + expect(ApiLogsLogic.actions.onPollStart).toHaveBeenCalled(); + + jest.advanceTimersByTime(5000); + expect(ApiLogsLogic.actions.fetchApiLogs).toHaveBeenCalledWith({ isPoll: true }); + }); + + it('does not create new polls if one already exists', () => { + mount({ intervalId: 123 }); + ApiLogsLogic.actions.pollForApiLogs(); + expect(setIntervalSpy).not.toHaveBeenCalled(); + }); + + afterAll(() => jest.useRealTimers); + }); + + describe('fetchApiLogs', () => { + const mockDate = jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1970-01-02').valueOf()); + + afterAll(() => mockDate.mockRestore()); + + it('should make an API call', () => { + mount(); + + ApiLogsLogic.actions.fetchApiLogs(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/api_logs', { + query: { + 'page[current]': 1, + 'filters[date][from]': '1970-01-01T00:00:00.000Z', + 'filters[date][to]': '1970-01-02T00:00:00.000Z', + sort_direction: 'desc', + }, + }); + }); + + describe('manual fetch (page load & pagination)', () => { + it('updates the view immediately with the returned data', async () => { + http.get.mockReturnValueOnce(Promise.resolve(MOCK_API_RESPONSE)); + mount(); + jest.spyOn(ApiLogsLogic.actions, 'updateView'); + + ApiLogsLogic.actions.fetchApiLogs(); + await nextTick(); + + expect(ApiLogsLogic.actions.updateView).toHaveBeenCalledWith(MOCK_API_RESPONSE); + }); + + it('handles API errors', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + mount(); + + ApiLogsLogic.actions.fetchApiLogs(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); + + describe('poll fetch (interval)', () => { + it('does not automatically update the view', async () => { + http.get.mockReturnValueOnce(Promise.resolve(MOCK_API_RESPONSE)); + mount({ dataLoading: false }); + jest.spyOn(ApiLogsLogic.actions, 'onPollInterval'); + + ApiLogsLogic.actions.fetchApiLogs({ isPoll: true }); + await nextTick(); + + expect(ApiLogsLogic.actions.onPollInterval).toHaveBeenCalledWith(MOCK_API_RESPONSE); + }); + + it('sets a custom error message on poll error', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + mount({ dataLoading: false }); + + ApiLogsLogic.actions.fetchApiLogs({ isPoll: true }); + await nextTick(); + + expect(flashErrorToast).toHaveBeenCalledWith('Could not refresh API log data', { + text: expect.stringContaining('Please check your connection'), + toastLifeTimeMs: 3750, + }); + }); + }); + + describe('when a manual fetch and a poll fetch occur at the same time', () => { + it('should short-circuit polls in favor of manual fetches', async () => { + // dataLoading is the signal we're using to check for a manual fetch + mount({ dataLoading: true }); + jest.spyOn(ApiLogsLogic.actions, 'onPollInterval'); + + ApiLogsLogic.actions.fetchApiLogs({ isPoll: true }); + await nextTick(); + + expect(http.get).not.toHaveBeenCalled(); + expect(ApiLogsLogic.actions.onPollInterval).not.toHaveBeenCalled(); + }); + }); + }); + + describe('onPollInterval', () => { + describe('when API logs are empty and new polled data comes in', () => { + it('updates the view immediately with the returned data (no manual action required)', () => { + mount({ meta: { page: { total_results: 0 } } }); + jest.spyOn(ApiLogsLogic.actions, 'updateView'); + + ApiLogsLogic.actions.onPollInterval(MOCK_API_RESPONSE); + + expect(ApiLogsLogic.actions.updateView).toHaveBeenCalledWith(MOCK_API_RESPONSE); + }); + }); + + describe('when previous API logs already exist on the page', () => { + describe('when new data is returned', () => { + it('stores the new polled data', () => { + mount({ meta: { page: { total_results: 1 } } }); + jest.spyOn(ApiLogsLogic.actions, 'storePolledData'); + + ApiLogsLogic.actions.onPollInterval(MOCK_API_RESPONSE); + + expect(ApiLogsLogic.actions.storePolledData).toHaveBeenCalledWith(MOCK_API_RESPONSE); + }); + }); + + describe('when the same data is returned', () => { + it('does nothing', () => { + mount({ meta: { page: { total_results: 100 } } }); + jest.spyOn(ApiLogsLogic.actions, 'updateView'); + jest.spyOn(ApiLogsLogic.actions, 'storePolledData'); + + ApiLogsLogic.actions.onPollInterval(MOCK_API_RESPONSE); + + expect(ApiLogsLogic.actions.updateView).not.toHaveBeenCalled(); + expect(ApiLogsLogic.actions.storePolledData).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('onUserRefresh', () => { + it('updates the apiLogs data with the stored polled data', () => { + mount({ apiLogs: [], polledData: MOCK_API_RESPONSE }); + + ApiLogsLogic.actions.onUserRefresh(); + + expect(ApiLogsLogic.values).toEqual({ + ...DEFAULT_VALUES, + apiLogs: MOCK_API_RESPONSE.results, + meta: MOCK_API_RESPONSE.meta, + polledData: MOCK_API_RESPONSE, + dataLoading: false, + }); + }); + }); + }); + + describe('events', () => { + describe('unmount', () => { + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + + it('clears the poll interval', () => { + mount({ intervalId: 123 }); + unmount(); + expect(clearIntervalSpy).toHaveBeenCalledWith(123); + }); + + it('does not clearInterval if a poll has not been started', () => { + mount({ intervalId: null }); + unmount(); + expect(clearIntervalSpy).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts new file mode 100644 index 0000000000000..a9186bd4d66cf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { flashAPIErrors, flashErrorToast } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { updateMetaPageIndex } from '../../../shared/table_pagination'; +import { EngineLogic } from '../engine'; + +import { POLLING_DURATION, POLLING_ERROR_TITLE, POLLING_ERROR_TEXT } from './constants'; +import { ApiLogsData, ApiLog } from './types'; +import { getDateString } from './utils'; + +interface ApiLogsValues { + dataLoading: boolean; + apiLogs: ApiLog[]; + meta: ApiLogsData['meta']; + hasNewData: boolean; + polledData: ApiLogsData; + intervalId: number | null; +} + +interface ApiLogsActions { + fetchApiLogs(options?: { isPoll: boolean }): { isPoll: boolean }; + pollForApiLogs(): void; + onPollStart(intervalId: number): { intervalId: number }; + onPollInterval(data: ApiLogsData): ApiLogsData; + storePolledData(data: ApiLogsData): ApiLogsData; + updateView(data: ApiLogsData): ApiLogsData; + onUserRefresh(): void; + onPaginate(newPageIndex: number): { newPageIndex: number }; +} + +export const ApiLogsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'api_logs_logic'], + actions: () => ({ + fetchApiLogs: ({ isPoll } = { isPoll: false }) => ({ isPoll }), + pollForApiLogs: true, + onPollStart: (intervalId) => ({ intervalId }), + onPollInterval: ({ results, meta }) => ({ results, meta }), + storePolledData: ({ results, meta }) => ({ results, meta }), + updateView: ({ results, meta }) => ({ results, meta }), + onUserRefresh: true, + onPaginate: (newPageIndex) => ({ newPageIndex }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + updateView: () => false, + onPaginate: () => true, + }, + ], + apiLogs: [ + [], + { + updateView: (_, { results }) => results, + }, + ], + meta: [ + DEFAULT_META, + { + updateView: (_, { meta }) => meta, + onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + }, + ], + hasNewData: [ + false, + { + storePolledData: () => true, + updateView: () => false, + }, + ], + polledData: [ + {} as ApiLogsData, + { + storePolledData: (_, data) => data, + }, + ], + intervalId: [ + null, + { + onPollStart: (_, { intervalId }) => intervalId, + }, + ], + }), + listeners: ({ actions, values }) => ({ + pollForApiLogs: () => { + if (values.intervalId) return; // Ensure we only have one poll at a time + + const id = window.setInterval(() => actions.fetchApiLogs({ isPoll: true }), POLLING_DURATION); + actions.onPollStart(id); + }, + fetchApiLogs: async ({ isPoll }) => { + if (isPoll && values.dataLoading) return; // Manual fetches (i.e. user pagination) should override polling + + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}/api_logs`, { + query: { + 'page[current]': values.meta.page.current, + 'filters[date][from]': getDateString(-1), + 'filters[date][to]': getDateString(), + sort_direction: 'desc', + }, + }); + + // Manual fetches (e.g. page load, user pagination) should update the view immediately, + // while polls are stored in-state until the user manually triggers the 'Refresh' action + if (isPoll) { + actions.onPollInterval(response); + } else { + actions.updateView(response); + } + } catch (e) { + if (isPoll) { + // If polling fails, it will typically be due to http connection - + // we should send a more human-readable message if so + flashErrorToast(POLLING_ERROR_TITLE, { + text: POLLING_ERROR_TEXT, + toastLifeTimeMs: POLLING_DURATION * 0.75, + }); + } else { + flashAPIErrors(e); + } + } + }, + onPollInterval: (data, breakpoint) => { + breakpoint(); // Prevents errors if logic unmounts while fetching + + const previousResults = values.meta.page.total_results; + const newResults = data.meta.page.total_results; + const isEmpty = previousResults === 0; + const hasNewData = previousResults !== newResults; + + if (isEmpty && hasNewData) { + actions.updateView(data); // Empty logs should automatically update with new data without a manual action + } else if (hasNewData) { + actions.storePolledData(data); // Otherwise, store any new data until the user manually refreshes the table + } + }, + onUserRefresh: () => { + actions.updateView(values.polledData); + }, + }), + events: ({ values }) => ({ + beforeUnmount() { + if (values.intervalId !== null) clearInterval(values.intervalId); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss new file mode 100644 index 0000000000000..44834d81a13c6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.scss @@ -0,0 +1,5 @@ +.apiLogDetailButton { + // More closely mimics the regular line height of an EuiLink / + // compresses table rows back to the standard height + height: $euiSizeL !important; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx new file mode 100644 index 0000000000000..768295ec1389c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions, mountWithIntl } from '../../../../__mocks__'; + +// NOTE: We're mocking FormattedRelative here because it (currently) has +// console warn issues, and it allows us to skip mocking dates +jest.mock('@kbn/i18n/react', () => ({ + ...(jest.requireActual('@kbn/i18n/react') as object), + FormattedRelative: jest.fn(() => '20 hours ago'), +})); + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +import { ApiLogsTable } from './'; + +describe('ApiLogsTable', () => { + const apiLogs = [ + { + timestamp: '1970-01-01T00:00:00.000Z', + status: 404, + http_method: 'GET', + full_request_path: '/api/as/v1/test', + }, + { + timestamp: '1970-01-01T00:00:00.000Z', + status: 500, + http_method: 'DELETE', + full_request_path: '/api/as/v1/test', + }, + { + timestamp: '1970-01-01T00:00:00.000Z', + status: 200, + http_method: 'POST', + full_request_path: '/api/as/v1/engines/some-engine/search', + }, + ]; + + const values = { + dataLoading: false, + apiLogs, + meta: DEFAULT_META, + }; + const actions = { + onPaginate: jest.fn(), + openFlyout: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = mountWithIntl(); + const tableContent = wrapper.find(EuiBasicTable).text(); + + expect(tableContent).toContain('Method'); + expect(tableContent).toContain('GET'); + expect(tableContent).toContain('DELETE'); + expect(tableContent).toContain('POST'); + expect(wrapper.find(EuiBadge)).toHaveLength(3); + + expect(tableContent).toContain('Time'); + expect(tableContent).toContain('20 hours ago'); + + expect(tableContent).toContain('Endpoint'); + expect(tableContent).toContain('/api/as/v1/test'); + expect(tableContent).toContain('/api/as/v1/engines/some-engine/search'); + + expect(tableContent).toContain('Status'); + expect(tableContent).toContain('404'); + expect(tableContent).toContain('500'); + expect(tableContent).toContain('200'); + expect(wrapper.find(EuiHealth)).toHaveLength(3); + + expect(wrapper.find(EuiButtonEmpty)).toHaveLength(3); + wrapper.find('[data-test-subj="ApiLogsTableDetailsButton"]').first().simulate('click'); + expect(actions.openFlyout).toHaveBeenCalled(); + }); + + it('renders an empty prompt if no items are passed', () => { + setMockValues({ ...values, apiLogs: [] }); + const wrapper = mountWithIntl(); + const promptContent = wrapper.find(EuiEmptyPrompt).text(); + + expect(promptContent).toContain('No recent logs'); + }); + + describe('hasPagination', () => { + it('does not render with pagination by default', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toBeFalsy(); + }); + + it('renders pagination if hasPagination is true', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('pagination')).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx new file mode 100644 index 0000000000000..5ecf8e1ba3330 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 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 { useValues, useActions } from 'kea'; + +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiHealth, + EuiButtonEmpty, + EuiEmptyPrompt, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative } from '@kbn/i18n/react'; + +import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; + +import { ApiLogLogic } from '../api_log'; +import { ApiLogsLogic } from '../index'; +import { ApiLog } from '../types'; +import { getStatusColor } from '../utils'; + +import './api_logs_table.scss'; + +interface Props { + hasPagination?: boolean; +} +export const ApiLogsTable: React.FC = ({ hasPagination }) => { + const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic); + const { onPaginate } = useActions(ApiLogsLogic); + const { openFlyout } = useActions(ApiLogLogic); + + const columns: Array> = [ + { + field: 'http_method', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.methodTableHeading', { + defaultMessage: 'Method', + }), + width: '100px', + render: (method: string) => {method}, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.timeTableHeading', { + defaultMessage: 'Time', + }), + width: '20%', + render: (dateString: string) => , + }, + { + field: 'full_request_path', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.endpointTableHeading', { + defaultMessage: 'Endpoint', + }), + width: '50%', + truncateText: true, + mobileOptions: { + // @ts-ignore - EUI's typing is incorrect here + width: '100%', + }, + }, + { + field: 'status', + name: i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.statusTableHeading', { + defaultMessage: 'Status', + }), + dataType: 'number', + width: '100px', + render: (status: number) => {status}, + }, + { + width: '100px', + align: 'right', + render: (apiLog: ApiLog) => ( + openFlyout(apiLog)} + > + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.detailsButtonLabel', { + defaultMessage: 'Details', + })} + + ), + }, + ]; + + const paginationProps = hasPagination + ? { + pagination: { + ...convertMetaToPagination(meta), + hidePerPageOptions: true, + }, + onChange: handlePageChange(onPaginate), + } + : {}; + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { + defaultMessage: 'No recent logs', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', { + defaultMessage: "Check back after you've performed some API calls.", + })} +

+ } + /> + } + {...paginationProps} + /> + ); +}; diff --git a/x-pack/plugins/banners/public/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts similarity index 68% rename from x-pack/plugins/banners/public/types.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts index 1f0ce524a785e..c0edc51d06228 100644 --- a/x-pack/plugins/banners/public/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/index.ts @@ -5,8 +5,5 @@ * 2.0. */ -import { BannerPlacement } from '../common'; - -export interface BannerClientConfig { - placement: BannerPlacement; -} +export { ApiLogsTable } from './api_logs_table'; +export { NewApiEventsPrompt } from './new_api_events_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss new file mode 100644 index 0000000000000..0f033bd37c61c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.scss @@ -0,0 +1,6 @@ +.newApiEventsPrompt { + padding: $euiSizeXS; + padding-left: $euiSizeS; + display: flex; + align-items: center; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx new file mode 100644 index 0000000000000..91d1962cd91db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonEmpty } from '@elastic/eui'; + +import { NewApiEventsPrompt } from './'; + +describe('NewApiEventsPrompt', () => { + const values = { + hasNewData: true, + }; + const actions = { + onUserRefresh: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('does not render if no new data has been polled', () => { + setMockValues({ ...values, hasNewData: false }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('calls onUserRefresh', () => { + const wrapper = shallow(); + + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(actions.onUserRefresh).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx new file mode 100644 index 0000000000000..1f834e061bd2c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/new_api_events_prompt.tsx @@ -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 React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiPanel, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ApiLogsLogic } from '../'; + +import './new_api_events_prompt.scss'; + +export const NewApiEventsPrompt: React.FC = () => { + const { hasNewData } = useValues(ApiLogsLogic); + const { onUserRefresh } = useActions(ApiLogsLogic); + + return hasNewData ? ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.apiLogs.newEventsMessage', { + defaultMessage: 'New events have been logged.', + })} + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.apiLogs.newEventsButtonLabel', { + defaultMessage: 'Refresh', + })} + + + ) : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts index 1620b0a953d46..ac1fbff150723 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts @@ -16,3 +16,14 @@ export const RECENT_API_EVENTS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.apiLogs.recent', { defaultMessage: 'Recent API events' } ); + +export const POLLING_DURATION = 5000; + +export const POLLING_ERROR_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorMessage', + { defaultMessage: 'Could not refresh API log data' } +); +export const POLLING_ERROR_TEXT = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorDescription', + { defaultMessage: 'Please check your connection or manually reload the page.' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts index 104ae03b89220..568026dab231f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/index.ts @@ -6,4 +6,7 @@ */ export { API_LOGS_TITLE } from './constants'; +export { ApiLogsTable, NewApiEventsPrompt } from './components'; +export { ApiLogFlyout } from './api_log'; export { ApiLogs } from './api_logs'; +export { ApiLogsLogic } from './api_logs_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/types.ts new file mode 100644 index 0000000000000..05c0d11d03240 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 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 } from '../../../../../common/types'; + +export interface ApiLog { + timestamp: string; // Date ISO string + status: number; + http_method: string; + full_request_path: string; + user_agent: string; + request_body: string; // JSON string + response_body: string; // JSON string + // NOTE: The API also sends us back `path: null`, but we don't appear to be + // using it anywhere, so I've opted not to list it in our types +} + +export interface ApiLogsData { + results: ApiLog[]; + meta: Meta; + // NOTE: The API sends us back even more `meta` data than the normal (sort_direction, filters, query), + // but we currently don't use that data in our front-end code, so I'm opting not to list them in our types +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts new file mode 100644 index 0000000000000..ac464e2af353d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dedent from 'dedent'; + +import { getDateString, getStatusColor, attemptToFormatJson } from './utils'; + +describe('getDateString', () => { + const mockDate = jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1970-01-02').valueOf()); + + it('gets the current date in ISO format', () => { + expect(getDateString()).toEqual('1970-01-02T00:00:00.000Z'); + }); + + it('allows passing a number of days to offset the timestamp by', () => { + expect(getDateString(-1)).toEqual('1970-01-01T00:00:00.000Z'); + expect(getDateString(10)).toEqual('1970-01-12T00:00:00.000Z'); + }); + + afterAll(() => mockDate.mockRestore()); +}); + +describe('getStatusColor', () => { + it('returns a valid EUI badge color based on the status code', () => { + expect(getStatusColor(200)).toEqual('secondary'); + expect(getStatusColor(301)).toEqual('primary'); + expect(getStatusColor(404)).toEqual('warning'); + expect(getStatusColor(503)).toEqual('danger'); + }); +}); + +describe('attemptToFormatJson', () => { + it('takes an unformatted JSON string and correctly newlines/indents it', () => { + expect(attemptToFormatJson('{"hello":"world","lorem":{"ipsum":"dolor","sit":"amet"}}')) + .toEqual(dedent`{ + "hello": "world", + "lorem": { + "ipsum": "dolor", + "sit": "amet" + } + }`); + }); + + it('returns the original content if it is not properly formatted JSON', () => { + expect(attemptToFormatJson('{invalid json}')).toEqual('{invalid json}'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.ts new file mode 100644 index 0000000000000..7e5f19686f13b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/utils.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. + */ + +export const getDateString = (offSetDays?: number) => { + const date = new Date(Date.now()); + if (offSetDays) date.setDate(date.getDate() + offSetDays); + return date.toISOString(); +}; + +export const getStatusColor = (status: number) => { + let color = ''; + if (status >= 100 && status < 300) color = 'secondary'; + if (status >= 300 && status < 400) color = 'primary'; + if (status >= 400 && status < 500) color = 'warning'; + if (status >= 500) color = 'danger'; + return color; +}; + +export const attemptToFormatJson = (possibleJson: string) => { + try { + // it is JSON, we can format it with newlines/indentation + return JSON.stringify(JSON.parse(possibleJson), null, 2); + } catch { + // if it's not JSON, we return the original content + return possibleJson; + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index 72cbe5bdd898c..8918445982ea6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -57,7 +57,7 @@ export const Credentials: React.FC = () => { {shouldShowCredentialsForm && } - +

{i18n.translate('xpack.enterpriseSearch.appSearch.credentials.apiEndpoint', { @@ -116,7 +116,9 @@ export const Credentials: React.FC = () => { - {!!dataLoading ? : } + + {!!dataLoading ? : } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx index 0d6ebfe437927..7e40eb63338bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx @@ -104,7 +104,7 @@ export const EngineSelection: React.FC = () => { return ( <> - +

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx index 0b631089c3984..f363f6978db29 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx @@ -22,7 +22,7 @@ export const FormKeyReadWriteAccess: React.FC = () => { return ( <> - +

{i18n.translate('xpack.enterpriseSearch.appSearch.credentials.formReadWrite.label', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx index b1bfc6c2ab7fa..10f1fc093e60f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx @@ -25,7 +25,7 @@ export const CurationCreation: React.FC = () => { <> - +

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index a523a683c4f5b..624790c847167 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -54,7 +54,7 @@ export const Curations: React.FC = () => { , ]} /> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx index c111383816e36..8034b72d885da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.test.tsx @@ -94,6 +94,14 @@ describe('DataPanel', () => { expect(wrapper.find(LoadingOverlay)).toHaveLength(1); }); + it('passes hasBorder', () => { + const wrapper = shallow(Test

} />); + expect(wrapper.prop('hasBorder')).toBeFalsy(); + + wrapper.setProps({ hasBorder: true }); + expect(wrapper.prop('hasBorder')).toBeTruthy(); + }); + it('passes class names', () => { const wrapper = shallow(Test

} className="testing" />); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx index 825311fa1652a..ce878dc3cf29a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/data_panel/data_panel.tsx @@ -29,6 +29,7 @@ interface Props { iconType?: string; action?: React.ReactNode; filled?: boolean; + hasBorder?: boolean; isLoading?: boolean; className?: string; } @@ -39,6 +40,7 @@ export const DataPanel: React.FC = ({ iconType, action, filled, + hasBorder, isLoading, className, children, @@ -52,6 +54,7 @@ export const DataPanel: React.FC = ({ {

- + POST diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx index dd55c26b5b298..fefe983df3342 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx @@ -91,7 +91,7 @@ export const DocumentDetail: React.FC = ({ engineBreadcrumb }) => { , ]} /> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 49f51c2010e3a..96fcd8997f674 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -6,6 +6,7 @@ */ import { setMockValues } from '../../../../__mocks__/kea.mock'; +import { setMockSearchContextState } from './__mocks__/hooks.mock'; import React from 'react'; @@ -16,7 +17,6 @@ import { Results } from '@elastic/react-search-ui'; import { SchemaTypes } from '../../../../shared/types'; -import { setMockSearchContextState } from './__mocks__/hooks.mock'; import { Pagination } from './pagination'; import { SearchExperienceContent } from './search_experience_content'; import { ResultView } from './views'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index bab31d0fccc40..4e1d7bc3e8e48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -16,12 +16,11 @@ import { EuiFlexItem, EuiFieldText, EuiSelect, - EuiPageBody, EuiPageHeader, + EuiPageContent, EuiSpacer, EuiTitle, EuiButton, - EuiPanel, } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; @@ -48,75 +47,73 @@ export const EngineCreation: React.FC = () => {
- - - - -
{ - e.preventDefault(); - submitEngine(); - }} - > - -

{ENGINE_CREATION_FORM_TITLE}

-
- - - - 0 && rawName !== name ? ( - <> - {SANITIZED_NAME_NOTE} {name} - - ) : ( - ALLOWED_CHARS_NOTE - ) - } + + + + { + e.preventDefault(); + submitEngine(); + }} + > + +

{ENGINE_CREATION_FORM_TITLE}

+
+ + + + 0 && rawName !== name ? ( + <> + {SANITIZED_NAME_NOTE} {name} + + ) : ( + ALLOWED_CHARS_NOTE + ) + } + fullWidth + > + setRawName(event.currentTarget.value)} + autoComplete="off" fullWidth - > - setRawName(event.currentTarget.value)} - autoComplete="off" - fullWidth - data-test-subj="EngineCreationNameInput" - placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER} - autoFocus - /> - - - - - setLanguage(event.currentTarget.value)} - /> - - - - - - {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} - - -
-
-
+ data-test-subj="EngineCreationNameInput" + placeholder={ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER} + autoFocus + /> + + + + + setLanguage(event.currentTarget.value)} + /> + + + + + + {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} + + + +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx index ceda3ab92f589..6f3ec806a438d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx @@ -5,24 +5,39 @@ * 2.0. */ +import { setMockActions } from '../../../../__mocks__'; +import '../../../../__mocks__/shallow_useeffect.mock'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { ApiLogsTable } from '../../api_logs'; + import { RecentApiLogs } from './recent_api_logs'; describe('RecentApiLogs', () => { + const actions = { + fetchApiLogs: jest.fn(), + pollForApiLogs: jest.fn(), + }; + let wrapper: ShallowWrapper; beforeAll(() => { jest.clearAllMocks(); + setMockActions(actions); wrapper = shallow(); }); it('renders the recent API logs table', () => { expect(wrapper.prop('title')).toEqual(

Recent API events

); - // TODO: expect(wrapper.find(ApiLogsTable)).toHaveLength(1) + expect(wrapper.find(ApiLogsTable)).toHaveLength(1); + }); + + it('calls fetchApiLogs on page load and starts pollForApiLogs', () => { + expect(actions.fetchApiLogs).toHaveBeenCalledTimes(1); + expect(actions.pollForApiLogs).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index 1c7f43a592536..18f27c3a1e834 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -5,10 +5,15 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useActions } from 'kea'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; +import { ApiLogsLogic, ApiLogsTable, NewApiEventsPrompt, ApiLogFlyout } from '../../api_logs'; import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { DataPanel } from '../../data_panel'; import { generateEnginePath } from '../../engine'; @@ -16,17 +21,32 @@ import { generateEnginePath } from '../../engine'; import { VIEW_API_LOGS } from '../constants'; export const RecentApiLogs: React.FC = () => { + const { fetchApiLogs, pollForApiLogs } = useActions(ApiLogsLogic); + + useEffect(() => { + fetchApiLogs(); + pollForApiLogs(); + }, []); + return ( {RECENT_API_EVENTS}

} action={ - - {VIEW_API_LOGS} - + + + + + + + {VIEW_API_LOGS} + + + } + hasBorder > - TODO: API Logs Table - {/* */} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx index 77ba9ad0f9514..136c9c6603e5c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx @@ -40,6 +40,7 @@ export const TotalCharts: React.FC = () => { {VIEW_ANALYTICS} } + hasBorder > { {VIEW_API_LOGS} } + hasBorder > { <> - + {canManageEngines ? ( { <> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index baf275fbe6c2c..a09f30035bafc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -84,7 +84,7 @@ export const EnginesOverview: React.FC = () => { - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx index d7fde0cd5dd25..b193e00c1d48d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx @@ -19,7 +19,7 @@ export const ErrorConnecting: React.FC = () => { - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 5e268cc0fd214..ad693628d911e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -86,7 +86,7 @@ export const Library: React.FC = () => { <> - +

Result

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx index a3dbf7259975b..85c24f1e42368 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx @@ -87,7 +87,7 @@ export const MetaEngineCreation: React.FC = () => { } /> - + = ({ boost, index, name }) => { }; return ( - + + {getBoostForm()} - + = ({ name, type, boosts = [] }) => { ); return ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss index 9795564da04d5..065effef9dded 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss @@ -19,7 +19,6 @@ } .relevanceTuningAccordionItem { - border: none; border-top: $euiBorderThin; border-radius: 0; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx index ab72f29a678c9..39200a699b3f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx @@ -73,7 +73,7 @@ export const RelevanceTuningForm: React.FC = () => { )} {filteredSchemaFields.map((fieldName) => ( - + { {filteredSchemaFieldsWithConflicts.map((fieldName) => ( - +

{fieldName}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx index 298b692ac7b80..5e5ee2ea8d0f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -48,7 +48,7 @@ export const RelevanceTuningPreview: React.FC = () => { const { engineName, isMetaEngine } = useValues(EngineLogic); return ( - +

{i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.title', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts index 717cdc952fd67..2d0515146b089 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/constants.ts @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { FieldResultSetting } from './types'; export const DEFAULT_SNIPPET_SIZE = 100; +export const SIZE_FIELD_MINIMUM = 20; +export const SIZE_FIELD_MAXIMUM = 1000; export const RESULT_SETTINGS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index 8ef7076927307..3388894c230a0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -5,19 +5,33 @@ * 2.0. */ +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockActions } from '../../../__mocks__'; + import React from 'react'; import { shallow } from 'enzyme'; import { ResultSettings } from './result_settings'; +import { ResultSettingsTable } from './result_settings_table'; describe('RelevanceTuning', () => { + const actions = { + initializeResultSettingsData: jest.fn(), + }; beforeEach(() => { + setMockActions(actions); jest.clearAllMocks(); }); it('renders', () => { const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); + expect(wrapper.find(ResultSettingsTable).exists()).toBe(true); + }); + + it('initializes result settings data when mounted', () => { + shallow(); + expect(actions.initializeResultSettingsData).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 6d6d5b2609898..7f4373835f8d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -5,29 +5,46 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; -import { EuiPageHeader, EuiPageContentBody, EuiPageContent } from '@elastic/eui'; +import { useActions } from 'kea'; + +import { EuiPageHeader, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { RESULT_SETTINGS_TITLE } from './constants'; +import { ResultSettingsTable } from './result_settings_table'; + +import { SampleResponse } from './sample_response'; + +import { ResultSettingsLogic } from '.'; interface Props { engineBreadcrumb: string[]; } export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { + const { initializeResultSettingsData } = useActions(ResultSettingsLogic); + + useEffect(() => { + initializeResultSettingsData(); + }, []); + return ( <> - - - - - + + + + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx new file mode 100644 index 0000000000000..a2ef43908776e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright 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 { shallow } from 'enzyme'; + +import { EuiTableHeaderCell } from '@elastic/eui'; + +import { ColumnHeaders } from './column_headers'; + +describe('ColumnHeaders', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiTableHeaderCell).length).toBe(3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx new file mode 100644 index 0000000000000..b36d71a49de13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/column_headers.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiIconTip, EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const ColumnHeaders: React.FC = () => { + return ( + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.table.rawTitle', { + defaultMessage: 'Raw', + })} + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.highlightingTitle', + { + defaultMessage: 'Highlighting', + } + )} + tags for highlighting. Fallback will look for a snippet match, but fallback to an escaped raw value if none is found. Range is between 20-1000. Defaults to 100.', + } + )} + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.test.tsx new file mode 100644 index 0000000000000..db87ac20a4223 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.test.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 { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiTableRow } from '@elastic/eui'; + +import { DisabledFieldsBody } from './disabled_fields_body'; + +describe('DisabledFieldsBody', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({ + schemaConflicts: { + foo: { + text: ['engine1'], + number: ['engine2'], + }, + bar: { + text: ['engine1'], + number: ['engine2'], + }, + }, + }); + }); + + it('renders a table row for each field', () => { + const wrapper = shallow(); + const tableRows = wrapper.find(EuiTableRow); + + expect(tableRows.length).toBe(2); + expect(tableRows.at(0).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'foo' + ); + expect(tableRows.at(1).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'bar' + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx new file mode 100644 index 0000000000000..2f4ba0892784d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_body.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiTableRow, EuiTableRowCell, EuiText, EuiHealth } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '..'; + +export const DisabledFieldsBody: React.FC = () => { + const { schemaConflicts } = useValues(ResultSettingsLogic); + return ( + <> + {Object.keys(schemaConflicts).map((fieldName) => ( + + + + {fieldName} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.fieldTypeConflictText', + { + defaultMessage: 'Field-type conflict', + } + )} + + + + ))} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.test.tsx new file mode 100644 index 0000000000000..5237f9ae15c11 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { DisabledFieldsHeader } from './disabled_fields_header'; + +describe('DisabledFieldsHeader', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx new file mode 100644 index 0000000000000..1c0c1da3e4ef2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/disabled_fields_header.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiTableRow, EuiTableHeaderCell } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const DisabledFieldsHeader: React.FC = () => { + return ( + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.disabledFieldsTitle', + { defaultMessage: 'Disabled fields' } + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx new file mode 100644 index 0000000000000..3ac50d906e9c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.test.tsx @@ -0,0 +1,110 @@ +/* + * Copyright 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 { shallow } from 'enzyme'; + +import { EuiFieldNumber } from '@elastic/eui'; + +import { FieldResultSetting } from '../types'; + +import { FieldNumber } from './field_number'; + +describe('FieldNumber', () => { + const fieldSettings = { + raw: true, + rawSize: 29, + snippet: true, + snippetFallback: true, + snippetSize: 15, + }; + + const props = { + fieldSettings, + fieldName: 'foo', + fieldEnabledProperty: 'raw' as keyof FieldResultSetting, + fieldSizeProperty: 'rawSize' as keyof FieldResultSetting, + updateAction: jest.fn(), + clearAction: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('is rendered with its value set from [fieldSizeProperty] in fieldSettings', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(29); + }); + + it('has no value if [fieldSizeProperty] in fieldSettings has no value', () => { + const wrapper = shallow( + + ); + expect(wrapper.find(EuiFieldNumber).prop('value')).toEqual(''); + }); + + it('is disabled if the [fieldEnabledProperty] in fieldSettings is false', () => { + const wrapper = shallow( + + ); + expect(wrapper.find(EuiFieldNumber).prop('disabled')).toEqual(true); + }); + + it('will call updateAction when the value is changed', () => { + const wrapper = shallow(); + wrapper.simulate('change', { target: { value: '21' } }); + expect(props.updateAction).toHaveBeenCalledWith('foo', 21); + }); + + it('will call clearAction when the value is changed', () => { + const wrapper = shallow(); + wrapper.simulate('change', { target: { value: '' } }); + expect(props.clearAction).toHaveBeenCalledWith('foo'); + }); + + it('will call updateAction on blur', () => { + const wrapper = shallow(); + wrapper.simulate('blur', { target: { value: '21' } }); + expect(props.updateAction).toHaveBeenCalledWith('foo', 21); + }); + + it('will call updateAction on blur using the minimum possible value if the current value is something other than a number', () => { + const wrapper = shallow(); + wrapper.simulate('blur', { target: { value: '' } }); + expect(props.updateAction).toHaveBeenCalledWith('foo', 20); + }); + + it('will call updateAction on blur using the minimum possible value if the value is something lower than the minimum', () => { + const wrapper = shallow(); + wrapper.simulate('blur', { target: { value: 5 } }); + expect(props.updateAction).toHaveBeenCalledWith('foo', 20); + }); + + it('will call updateAction on blur using the maximum possible value if the value is something above than the maximum', () => { + const wrapper = shallow(); + wrapper.simulate('blur', { target: { value: 2000 } }); + expect(props.updateAction).toHaveBeenCalledWith('foo', 1000); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx new file mode 100644 index 0000000000000..cd7bab3c6f594 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/field_number.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 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, { ChangeEvent, FocusEvent } from 'react'; + +import { EuiFieldNumber } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { SIZE_FIELD_MAXIMUM, SIZE_FIELD_MINIMUM } from '../constants'; +import { FieldResultSetting } from '../types'; + +const updateOrClearSizeForField = ( + fieldName: string, + fieldValue: number, + updateAction: (fieldName: string, size: number) => void, + clearAction: (fieldName: string) => void +) => { + if (typeof fieldValue === 'number' && !isNaN(fieldValue)) { + updateAction(fieldName, fieldValue); + } else { + clearAction(fieldName); + } +}; + +const handleFieldNumberChange = ( + fieldName: string, + updateAction: (fieldName: string, size: number) => void, + clearAction: (fieldName: string) => void +) => { + return (e: ChangeEvent) => { + const fieldValue = parseInt(e.target.value, 10); + updateOrClearSizeForField(fieldName, fieldValue, updateAction, clearAction); + }; +}; + +const handleFieldNumberBlur = ( + fieldName: string, + updateAction: (fieldName: string, size: number) => void, + clearAction: (fieldName: string) => void +) => { + return (e: FocusEvent) => { + const value = parseInt(e.target.value, 10); + const fieldValue = Math.min( + SIZE_FIELD_MAXIMUM, + Math.max(SIZE_FIELD_MINIMUM, isNaN(value) ? 0 : value) + ); + updateOrClearSizeForField(fieldName, fieldValue, updateAction, clearAction); + }; +}; + +interface Props { + fieldSettings: Partial; + fieldName: string; + fieldEnabledProperty: keyof FieldResultSetting; + fieldSizeProperty: keyof FieldResultSetting; + updateAction: (fieldName: string, size: number) => void; + clearAction: (fieldName: string) => void; +} + +export const FieldNumber: React.FC = ({ + fieldSettings, + fieldName, + fieldEnabledProperty, + fieldSizeProperty, + updateAction, + clearAction, +}) => { + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/index.ts new file mode 100644 index 0000000000000..1fcbf05bcc0c7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/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 { ResultSettingsTable } from './result_settings_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.test.tsx new file mode 100644 index 0000000000000..c99b8644812b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.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 { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiTableRow } from '@elastic/eui'; + +import { NonTextFieldsBody } from './non_text_fields_body'; + +describe('NonTextFieldsBody', () => { + const values = { + nonTextResultFields: { + foo: { + raw: false, + }, + zoo: { + raw: true, + rawSize: 5, + }, + bar: { + raw: true, + rawSize: 5, + }, + }, + }; + + const actions = { + toggleRawForField: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + const getTableRows = (wrapper: ShallowWrapper) => wrapper.find(EuiTableRow); + const getBarTableRow = (wrapper: ShallowWrapper) => getTableRows(wrapper).at(0); + + it('renders a table row for each field, sorted by field name', () => { + const wrapper = shallow(); + const tableRows = getTableRows(wrapper); + + expect(tableRows.length).toBe(3); + expect(tableRows.at(0).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'bar' + ); + expect(tableRows.at(1).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'foo' + ); + expect(tableRows.at(2).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'zoo' + ); + }); + + describe('the "raw" checkbox within each table row', () => { + const getRawCheckbox = () => { + const wrapper = shallow(); + const tableRow = getBarTableRow(wrapper); + return tableRow.find('[data-test-subj="ResultSettingRawCheckBox"]'); + }; + + it('is rendered with its checked property set from state', () => { + const rawCheckbox = getRawCheckbox(); + expect(rawCheckbox.prop('checked')).toEqual(values.nonTextResultFields.bar.raw); + }); + + it("calls 'toggleRawForField' when it is clicked by a user", () => { + const rawCheckbox = getRawCheckbox(); + rawCheckbox.simulate('change'); + expect(actions.toggleRawForField).toHaveBeenCalledWith('bar'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx new file mode 100644 index 0000000000000..dc91b5039a3c9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_body.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiTableRow, EuiTableRowCell, EuiCheckbox, EuiTableRowCellCheckbox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '..'; +import { FieldResultSetting } from '../types'; + +export const NonTextFieldsBody: React.FC = () => { + const { nonTextResultFields } = useValues(ResultSettingsLogic); + const { toggleRawForField } = useActions(ResultSettingsLogic); + + const resultSettingsArray: Array<[string, Partial]> = useMemo(() => { + return Object.entries(nonTextResultFields).sort(([aFieldName], [bFieldName]) => + aFieldName > bFieldName ? 1 : -1 + ); + }, [nonTextResultFields]); + + return ( + <> + {resultSettingsArray.map(([fieldName, fieldSettings]) => ( + + + {fieldName} + + + { + toggleRawForField(fieldName); + }} + /> + + + + ))} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.test.tsx new file mode 100644 index 0000000000000..3c0920f29f884 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { NonTextFieldsHeader } from './non_text_fields_header'; + +describe('NonTextFieldsHeader', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx new file mode 100644 index 0000000000000..b929187780e10 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/non_text_fields_header.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiTableRow, EuiTableHeaderCell } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const NonTextFieldsHeader: React.FC = () => { + return ( + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.nonTextFieldsTitle', + { defaultMessage: 'Non-text fields' } + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.rawTitle', + { defaultMessage: 'Raw' } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.scss new file mode 100644 index 0000000000000..7779abe7eb6fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.scss @@ -0,0 +1,30 @@ +.resultSettingsTable { + table-layout: auto; + + &__columnLabels { + .euiTableHeaderCell { + border-bottom: none; + padding-bottom: $euiSizeM; + } + } + + .euiTableRow:last-of-type { + .euiTableRowCell, .euiTableRowCellCheckbox { + // Because we are using the large border-top as a way of spacing, and table borders + // are collapsed, these tables do not have a bottom border. The exception to that is + // if the tbody is the last tbody of the table. To make it consistent, we explicitly + // disable all bottom borders here + border-bottom: none; + } + } + + tbody + &__subHeader { + // Since this table has multiple sets of thead + tbody's, this is our way of keeping + // vertical space between the two + border-top: solid $euiSizeL * 2 transparent; + } + + .euiTableRowCellCheckbox .euiTableCellContent { + justify-content: center; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.test.tsx new file mode 100644 index 0000000000000..151d436e59ea2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.test.tsx @@ -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 { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { DisabledFieldsHeader } from './disabled_fields_header'; +import { NonTextFieldsBody } from './non_text_fields_body'; +import { ResultSettingsTable } from './result_settings_table'; +import { TextFieldsBody } from './text_fields_body'; + +describe('ResultSettingsTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues({ + textResultFields: { foo: { raw: true, rawSize: 5, snippet: false, snippetFallback: false } }, + nonTextResultFields: { + bar: { raw: true, rawSize: 5, snippet: false, snippetFallback: false }, + }, + schemaConflicts: { + foo: { + text: ['foo'], + number: ['foo'], + geolocation: [], + date: [], + }, + }, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(TextFieldsBody).exists()).toBe(true); + expect(wrapper.find(NonTextFieldsBody).exists()).toBe(true); + expect(wrapper.find(DisabledFieldsHeader).exists()).toBe(true); + }); + + it('will hide sections that have no data available to show', () => { + setMockValues({ + textResultFields: {}, + nonTextResultFields: {}, + schemaConflicts: {}, + }); + + const wrapper = shallow(); + expect(wrapper.find(TextFieldsBody).exists()).toBe(false); + expect(wrapper.find(NonTextFieldsBody).exists()).toBe(false); + expect(wrapper.find(DisabledFieldsHeader).exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx new file mode 100644 index 0000000000000..092a4beee0c8e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/result_settings_table.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiTable, EuiTableBody } from '@elastic/eui'; + +import { ResultSettingsLogic } from '..'; + +import { ColumnHeaders } from './column_headers'; +import { DisabledFieldsBody } from './disabled_fields_body'; +import { DisabledFieldsHeader } from './disabled_fields_header'; +import { NonTextFieldsBody } from './non_text_fields_body'; +import { NonTextFieldsHeader } from './non_text_fields_header'; +import { TextFieldsBody } from './text_fields_body'; +import { TextFieldsHeader } from './text_fields_header'; + +import './result_settings_table.scss'; + +export const ResultSettingsTable: React.FC = () => { + const { schemaConflicts, textResultFields, nonTextResultFields } = useValues(ResultSettingsLogic); + + // TODO This table currently has mutiple theads, which is invalid html. We could change these subheaders to be EuiTableRow instead of EuiTableHeader + // to alleviate the issue. + return ( + + + {!!Object.keys(textResultFields).length && ( + + + + + )} + {!!Object.keys(nonTextResultFields).length && ( + + + + + )} + {!!Object.keys(schemaConflicts).length && ( + + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.test.tsx new file mode 100644 index 0000000000000..7be58a387fa69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.test.tsx @@ -0,0 +1,141 @@ +/* + * Copyright 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 { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiTableRow } from '@elastic/eui'; + +import { TextFieldsBody } from './text_fields_body'; + +describe('TextFieldsBody', () => { + const values = { + textResultFields: { + foo: { + raw: false, + snippet: true, + snippetFallback: true, + snippetSize: 15, + }, + zoo: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + bar: { + raw: true, + rawSize: 5, + snippet: false, + snippetFallback: false, + }, + }, + }; + + const actions = { + toggleRawForField: jest.fn(), + updateRawSizeForField: jest.fn(), + clearRawSizeForField: jest.fn(), + toggleSnippetForField: jest.fn(), + updateSnippetSizeForField: jest.fn(), + clearSnippetSizeForField: jest.fn(), + toggleSnippetFallbackForField: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + const getTableRows = (wrapper: ShallowWrapper) => wrapper.find(EuiTableRow); + const getBarTableRow = (wrapper: ShallowWrapper) => getTableRows(wrapper).at(0); + const getFooTableRow = (wrapper: ShallowWrapper) => getTableRows(wrapper).at(1); + + it('renders a table row for each field, sorted by field name', () => { + const wrapper = shallow(); + const tableRows = getTableRows(wrapper); + + expect(tableRows.length).toBe(3); + expect(tableRows.at(0).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'bar' + ); + expect(tableRows.at(1).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'foo' + ); + expect(tableRows.at(2).find('[data-test-subj="ResultSettingFieldName"]').dive().text()).toEqual( + 'zoo' + ); + }); + + describe('the "raw" checkbox within each table row', () => { + const getRawCheckbox = () => { + const wrapper = shallow(); + const tableRow = getBarTableRow(wrapper); + return tableRow.find('[data-test-subj="ResultSettingRawCheckBox"]'); + }; + + it('is rendered with its checked property set from state', () => { + const rawCheckbox = getRawCheckbox(); + expect(rawCheckbox.prop('checked')).toEqual(values.textResultFields.bar.raw); + }); + + it("calls 'toggleRawForField' when it is clicked by a user", () => { + const rawCheckbox = getRawCheckbox(); + rawCheckbox.simulate('change'); + expect(actions.toggleRawForField).toHaveBeenCalledWith('bar'); + }); + }); + + describe('the "snippet" checkbox within each table row', () => { + const getSnippetCheckbox = () => { + const wrapper = shallow(); + const tableRow = getFooTableRow(wrapper); + return tableRow.find('[data-test-subj="ResultSettingSnippetTextBox"]'); + }; + + it('is rendered with its checked property set from state', () => { + const snippetCheckbox = getSnippetCheckbox(); + expect(snippetCheckbox.prop('checked')).toEqual(values.textResultFields.foo.snippet); + }); + + it("calls 'toggleRawForField' when it is clicked by a user", () => { + const snippetCheckbox = getSnippetCheckbox(); + snippetCheckbox.simulate('change'); + expect(actions.toggleSnippetForField).toHaveBeenCalledWith('foo'); + }); + }); + + describe('the "fallback" checkbox within each table row', () => { + const getFallbackCheckbox = () => { + const wrapper = shallow(); + const tableRow = getFooTableRow(wrapper); + return tableRow.find('[data-test-subj="ResultSettingFallbackTextBox"]'); + }; + + it('is rendered with its checked property set from state', () => { + const fallbackCheckbox = getFallbackCheckbox(); + expect(fallbackCheckbox.prop('checked')).toEqual(values.textResultFields.foo.snippetFallback); + }); + + it('is disabled if snippets are disabled for this field', () => { + const wrapper = shallow(); + const tableRow = getBarTableRow(wrapper); + const fallbackCheckbox = tableRow.find('[data-test-subj="ResultSettingFallbackTextBox"]'); + expect(fallbackCheckbox.prop('disabled')).toEqual(true); + }); + + it("calls 'toggleSnippetFallbackForField' when it is clicked by a user", () => { + const fallbackCheckbox = getFallbackCheckbox(); + fallbackCheckbox.simulate('change'); + expect(actions.toggleSnippetFallbackForField).toHaveBeenCalledWith('foo'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx new file mode 100644 index 0000000000000..3a2eb20fecdf0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_body.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiTableRow, EuiTableRowCell, EuiTableRowCellCheckbox, EuiCheckbox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; +import { FieldResultSetting } from '../types'; + +import { FieldNumber } from './field_number'; + +export const TextFieldsBody: React.FC = () => { + const { textResultFields } = useValues(ResultSettingsLogic); + const { + toggleRawForField, + updateRawSizeForField, + clearRawSizeForField, + toggleSnippetForField, + updateSnippetSizeForField, + clearSnippetSizeForField, + toggleSnippetFallbackForField, + } = useActions(ResultSettingsLogic); + + const resultSettingsArray: Array<[string, Partial]> = useMemo(() => { + return Object.entries(textResultFields).sort(([aFieldName], [bFieldName]) => + aFieldName > bFieldName ? 1 : -1 + ); + }, [textResultFields]); + + return ( + <> + {resultSettingsArray.map(([fieldName, fieldSettings]) => ( + + + {fieldName} + + + { + toggleRawForField(fieldName); + }} + /> + + + + + + { + toggleSnippetForField(fieldName); + }} + /> + + + { + toggleSnippetFallbackForField(fieldName); + }} + /> + + + + + + ))} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.test.tsx new file mode 100644 index 0000000000000..be7d18e375fdd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { TextFieldsHeader } from './text_fields_header'; + +describe('TextFieldsHeader', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx new file mode 100644 index 0000000000000..cf4dfa9462781 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_table/text_fields_header.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiTableRow, EuiTableHeaderCell } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +export const TextFieldsHeader: React.FC = () => { + return ( + <> + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.textFieldsTitle', + { defaultMessage: 'Text fields' } + )} + + {/* TODO Right now the stacked "Raw" ths leads screen readers to reading out Raw - Raw - Raw 3x in a row once you get down to the non-text fields. We should consider either: + Channging this "Raw" column to something like "Enabled" + Or losing the RAW vs HIGHLIGHTING top-level headings */} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.rawTitle', + { defaultMessage: 'Raw' } + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.maxSizeTitle', + { defaultMessage: 'Max size' } + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.snippetTitle', + { defaultMessage: 'Snippet' } + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.fallbackTitle', + { defaultMessage: 'Fallback' } + )} + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.table.column.maxSizeTitle', + { defaultMessage: 'Max size' } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/index.ts new file mode 100644 index 0000000000000..257ad27fe8748 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/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 { SampleResponse } from './sample_response'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.test.tsx new file mode 100644 index 0000000000000..e324150a2d52a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 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__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCodeBlock, EuiFieldSearch } from '@elastic/eui'; + +import { SampleResponse } from './sample_response'; + +describe('SampleResponse', () => { + const actions = { + queryChanged: jest.fn(), + getSearchResults: jest.fn(), + }; + + const values = { + reducedServerResultFields: {}, + query: 'foo', + response: { + bar: 'baz', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + setMockValues(values); + }); + + it('renders a text box with the current user "query" value from state', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('updates the "query" value in state when a user updates the text in the text box', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } }); + expect(actions.queryChanged).toHaveBeenCalledWith('bar'); + }); + + it('will call getSearchResults with the current value of query and reducedServerResultFields in a useEffect, which updates the displayed response', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).prop('value')).toEqual('foo'); + }); + + it('renders the response from the given user "query" in a code block', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('{\n "bar": "baz"\n}'); + }); + + it('renders a plain old string in the code block if the response is a string', () => { + setMockValues({ + response: 'No results.', + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).prop('children')).toEqual('No results.'); + }); + + it('will not render a code block at all if there is no response yet', () => { + setMockValues({ + response: null, + }); + const wrapper = shallow(); + expect(wrapper.find(EuiCodeBlock).exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.tsx new file mode 100644 index 0000000000000..ae91b9648356c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response.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, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiCodeBlock, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ResultSettingsLogic } from '../result_settings_logic'; + +import { SampleResponseLogic } from './sample_response_logic'; + +export const SampleResponse: React.FC = () => { + const { reducedServerResultFields } = useValues(ResultSettingsLogic); + + const { query, response } = useValues(SampleResponseLogic); + const { queryChanged, getSearchResults } = useActions(SampleResponseLogic); + + useEffect(() => { + getSearchResults(query, reducedServerResultFields); + }, [query, reducedServerResultFields]); + + return ( + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponseTitle', + { defaultMessage: 'Sample response' } + )} +

+
+
+ + {/* TODO */} + +
+ + queryChanged(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.inputPlaceholder', + { defaultMessage: 'Type a search query to test a response...' } + )} + data-test-subj="ResultSettingsQuerySampleResponse" + /> + + {!!response && ( + + {typeof response === 'string' ? response : JSON.stringify(response, null, 2)} + + )} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts new file mode 100644 index 0000000000000..79379306c1618 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright 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 { LogicMounter, mockHttpValues } from '../../../../__mocks__'; +import '../../../__mocks__/engine_logic.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { SampleResponseLogic } from './sample_response_logic'; + +describe('SampleResponseLogic', () => { + const { mount } = new LogicMounter(SampleResponseLogic); + const { http } = mockHttpValues; + + const DEFAULT_VALUES = { + query: '', + response: null, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + }); + }); + + describe('actions', () => { + describe('queryChanged', () => { + it('updates the query', () => { + mount({ + query: '', + }); + + SampleResponseLogic.actions.queryChanged('foo'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + query: 'foo', + }); + }); + }); + + describe('getSearchResultsSuccess', () => { + it('sets the response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsSuccess({}); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: {}, + }); + }); + }); + + describe('getSearchResultsFailure', () => { + it('sets a string response from a search API request', () => { + mount({ + response: null, + }); + + SampleResponseLogic.actions.getSearchResultsFailure('An error occured.'); + + expect(SampleResponseLogic.values).toEqual({ + ...DEFAULT_VALUES, + response: 'An error occured.', + }); + }); + }); + }); + + describe('listeners', () => { + describe('getSearchResults', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + it('makes a search API request and calls getSearchResultsSuccess with the first result of the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [ + { id: { raw: 'foo' }, _meta: {} }, + { id: { raw: 'bar' }, _meta: {} }, + { id: { raw: 'baz' }, _meta: {} }, + ], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith({ + // Note that the _meta field was stripped from the result + id: { raw: 'foo' }, + }); + }); + + it('calls getSearchResultsSuccess with a "No Results." message if there are no results', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + http.post.mockReturnValue( + Promise.resolve({ + results: [], + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).toHaveBeenCalledWith( + 'No results.' + ); + }); + + it('handles 500 errors by setting a generic error response and showing a flash message error', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + const error = { + response: { + status: 500, + }, + }; + + http.post.mockReturnValueOnce(Promise.reject(error)); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith(error); + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('handles 400 errors by setting the response, but does not show a flash error message', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + body: { + attributes: { + errors: ['A validation error occurred.'], + }, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith({ + errors: ['A validation error occurred.'], + }); + }); + + it('sets a generic message on a 400 error if no custom message is provided in the response', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsFailure'); + + http.post.mockReturnValueOnce( + Promise.reject({ + response: { + status: 400, + }, + }) + ); + + SampleResponseLogic.actions.getSearchResults('foo', { foo: { raw: true } }); + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsFailure).toHaveBeenCalledWith( + 'An error occured.' + ); + }); + + it('does nothing if an empty object is passed for the resultFields parameter', async () => { + mount(); + jest.spyOn(SampleResponseLogic.actions, 'getSearchResultsSuccess'); + + SampleResponseLogic.actions.getSearchResults('foo', {}); + + jest.runAllTimers(); + await nextTick(); + + expect(SampleResponseLogic.actions.getSearchResultsSuccess).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts new file mode 100644 index 0000000000000..808a7ec9c65dc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/sample_response/sample_response_logic.ts @@ -0,0 +1,100 @@ +/* + * Copyright 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 { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { HttpLogic } from '../../../../shared/http'; +import { EngineLogic } from '../../engine'; + +import { SampleSearchResponse, ServerFieldResultSettingObject } from '../types'; + +const NO_RESULTS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.noResultsMessage', + { defaultMessage: 'No results.' } +); + +const ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.sampleResponse.errorMessage', + { defaultMessage: 'An error occured.' } +); + +interface SampleResponseValues { + query: string; + response: SampleSearchResponse | string | null; +} + +interface SampleResponseActions { + queryChanged: (query: string) => { query: string }; + getSearchResultsSuccess: ( + response: SampleSearchResponse | string + ) => { response: SampleSearchResponse | string }; + getSearchResultsFailure: (response: string) => { response: string }; + getSearchResults: ( + query: string, + resultFields: ServerFieldResultSettingObject + ) => { query: string; resultFields: ServerFieldResultSettingObject }; +} + +export const SampleResponseLogic = kea>({ + path: ['enterprise_search', 'app_search', 'sample_response_logic'], + actions: { + queryChanged: (query) => ({ query }), + getSearchResultsSuccess: (response) => ({ response }), + getSearchResultsFailure: (response) => ({ response }), + getSearchResults: (query, resultFields) => ({ query, resultFields }), + }, + reducers: { + query: ['', { queryChanged: (_, { query }) => query }], + response: [ + null, + { + getSearchResultsSuccess: (_, { response }) => response, + getSearchResultsFailure: (_, { response }) => response, + }, + ], + }, + listeners: ({ actions }) => ({ + getSearchResults: async ({ query, resultFields }, breakpoint) => { + if (Object.keys(resultFields).length < 1) return; + await breakpoint(250); + + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + const url = `/api/app_search/engines/${engineName}/sample_response_search`; + + try { + const response = await http.post(url, { + body: JSON.stringify({ + query, + result_fields: resultFields, + }), + }); + + const result = response.results?.[0]; + actions.getSearchResultsSuccess( + result ? { ...result, _meta: undefined } : NO_RESULTS_MESSAGE + ); + } catch (e) { + if (e.response.status >= 500) { + // 4XX Validation errors are expected, as a user could enter something like 2 as a size, which is out of valid range. + // In this case, we simply render the message from the server as the response. + // + // 5xx Server errors are unexpected, and need to be reported in a flash message. + flashAPIErrors(e); + actions.getSearchResultsFailure(ERROR_MESSAGE); + } else { + actions.getSearchResultsFailure(e.body?.attributes || ERROR_MESSAGE); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts index 96bf277314a7b..18843112f46bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FieldValue } from '../result/types'; + export enum OpenModal { None, ConfirmResetModal, @@ -35,3 +37,5 @@ export interface FieldResultSetting { } export type FieldResultSettingObject = Record; + +export type SampleSearchResponse = Record; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index bfa3fefb2732d..ebd034caaedb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -166,7 +166,7 @@ export const RoleMapping: React.FC = ({ isNew }) => { - +

{ROLE_TITLE}

@@ -175,7 +175,6 @@ export const RoleMapping: React.FC = ({ isNew }) => {

{FULL_ENGINE_ACCESS_TITLE}

- export{' '} {STANDARD_ROLE_TYPES.map(({ type, description }) => ( = ({ isNew }) => {
{hasAdvancedRoles && ( - +

{ENGINE_ACCESS_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index e31f5c04bdb45..2ec2b93d1e24f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -127,7 +127,7 @@ export const RoleMappings: React.FC = () => { pageTitle={ROLE_MAPPINGS_TITLE} description={ROLE_MAPPINGS_DESCRIPTION} /> - + {roleMappings.length === 0 ? roleMappingEmptyState : roleMappingsTable} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx index 8de6b6030ef66..6f1ccd1ae2b53 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/sample_engine_creation_cta/sample_engine_creation_cta.tsx @@ -23,9 +23,9 @@ export const SampleEngineCreationCta: React.FC = () => { const { createSampleEngine } = useActions(SampleEngineCreationCtaLogic); return ( - - - + + +

{SAMPLE_ENGINE_CREATION_CTA_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index 88b62b8ae83f7..2d5dd08f81288 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -21,7 +21,7 @@ export const Settings: React.FC = () => { <> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 155ff5b92ba27..c2bf77751528a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -49,6 +49,7 @@ export const renderApp = ( history: params.history, navigateToUrl: core.application.navigateToUrl, setBreadcrumbs: core.chrome.setBreadcrumbs, + setChromeIsVisible: core.chrome.setIsVisible, setDocTitle: core.chrome.docTitle.change, renderHeaderActions: (HeaderActions) => params.setHeaderActionMenu((el) => renderHeaderActions(HeaderActions, store, el)), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.ts new file mode 100644 index 0000000000000..35e1942bdc3de --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/constants.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FlashMessageColors } from './types'; + +export const FLASH_MESSAGE_TYPES = { + success: { color: 'success' as FlashMessageColors, iconType: 'check' }, + info: { color: 'primary' as FlashMessageColors, iconType: 'iInCircle' }, + warning: { color: 'warning' as FlashMessageColors, iconType: 'alert' }, + error: { color: 'danger' as FlashMessageColors, iconType: 'alert' }, +}; + +// This is the default amount of time (5 seconds) a toast will last before disappearing +// It can be overridden per-toast by passing the `toastLifetimeMs` property - @see types.ts +export const DEFAULT_TOAST_TIMEOUT = 5000; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx index aa45ce58af86a..289dcc0137cb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -5,62 +5,96 @@ * 2.0. */ -import { setMockValues } from '../../__mocks__/kea.mock'; +import { setMockValues, setMockActions } from '../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiGlobalToastList } from '@elastic/eui'; -import { FlashMessages } from './flash_messages'; +import { FlashMessages, Callouts, Toasts } from './flash_messages'; describe('FlashMessages', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('does not render if no messages exist', () => { - setMockValues({ messages: [] }); - + it('renders callout and toast flash messages', () => { const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); + expect(wrapper.find(Callouts)).toHaveLength(1); + expect(wrapper.find(Toasts)).toHaveLength(1); }); - it('renders an array of flash messages & types', () => { - const mockMessages = [ - { type: 'success', message: 'Hello world!!' }, - { - type: 'error', - message: 'Whoa nelly!', - description:
Something went wrong
, - }, - { type: 'info', message: 'Everything is fine, nothing is ruined' }, - { type: 'warning', message: 'Uh oh' }, - { type: 'info', message: 'Testing multiples of same type' }, - ]; - setMockValues({ messages: mockMessages }); + describe('callouts', () => { + it('renders an array of flash messages & types', () => { + const mockMessages = [ + { type: 'success', message: 'Hello world!!' }, + { + type: 'error', + message: 'Whoa nelly!', + description:
Something went wrong
, + }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + { type: 'warning', message: 'Uh oh' }, + { type: 'info', message: 'Testing multiples of same type' }, + ]; + setMockValues({ messages: mockMessages }); - const wrapper = shallow(); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(5); + expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); + expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); + expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); + }); - expect(wrapper.find(EuiCallOut)).toHaveLength(5); - expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success'); - expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1); - expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle'); + it('renders any children', () => { + setMockValues({ messages: [{ type: 'success' }] }); + + const wrapper = shallow( + + + + ); + + expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + }); }); - it('renders any children', () => { - setMockValues({ messages: [{ type: 'success' }] }); + describe('toasts', () => { + const actions = { dismissToastMessage: jest.fn() }; + beforeAll(() => setMockActions(actions)); + + it('renders an EUI toast list', () => { + const mockToasts = [ + { id: 'test', title: 'Hello world!!' }, + { + color: 'success', + iconType: 'check', + title: 'Success!', + toastLifeTimeMs: 500, + id: 'successToastId', + }, + { + color: 'danger', + iconType: 'alert', + title: 'Oh no!', + text:
Something went wrong
, + id: 'errorToastId', + }, + ]; + setMockValues({ toastMessages: mockToasts }); - const wrapper = shallow( - - - - ); + const wrapper = shallow(); + const euiToastList = wrapper.find(EuiGlobalToastList); - expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); + expect(euiToastList).toHaveLength(1); + expect(euiToastList.prop('toasts')).toEqual(mockToasts); + expect(euiToastList.prop('dismissToast')).toEqual(actions.dismissToastMessage); + expect(euiToastList.prop('toastLifeTimeMs')).toEqual(5000); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index ef1a4a2d0be86..10f80d9a6345a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -7,32 +7,30 @@ import React, { Fragment } from 'react'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; -import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer, EuiGlobalToastList } from '@elastic/eui'; +import { FLASH_MESSAGE_TYPES, DEFAULT_TOAST_TIMEOUT } from './constants'; import { FlashMessagesLogic } from './flash_messages_logic'; -const FLASH_MESSAGE_TYPES = { - success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' }, - info: { color: 'primary' as EuiCallOutProps['color'], icon: 'iInCircle' }, - warning: { color: 'warning' as EuiCallOutProps['color'], icon: 'alert' }, - error: { color: 'danger' as EuiCallOutProps['color'], icon: 'alert' }, -}; +export const FlashMessages: React.FC = ({ children }) => ( + <> + {children} + + +); -export const FlashMessages: React.FC = ({ children }) => { +export const Callouts: React.FC = ({ children }) => { const { messages } = useValues(FlashMessagesLogic); - // If we have no messages to display, do not render the element at all - if (!messages.length) return null; - return ( -
+
{messages.map(({ type, message, description }, index) => ( {description} @@ -44,3 +42,16 @@ export const FlashMessages: React.FC = ({ children }) => {
); }; + +export const Toasts: React.FC = () => { + const { toastMessages } = useValues(FlashMessagesLogic); + const { dismissToastMessage } = useActions(FlashMessagesLogic); + + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts index 7fc78c99fb242..c7dc658dada74 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -15,11 +15,13 @@ import { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_lo import { IFlashMessage } from './types'; describe('FlashMessagesLogic', () => { - const mount = () => mountFlashMessagesLogic(); + const mount = () => { + resetContext({}); + return mountFlashMessagesLogic(); + }; beforeEach(() => { jest.clearAllMocks(); - resetContext({}); }); it('has default values', () => { @@ -27,67 +29,112 @@ describe('FlashMessagesLogic', () => { expect(FlashMessagesLogic.values).toEqual({ messages: [], queuedMessages: [], + toastMessages: [], historyListener: expect.any(Function), }); }); - describe('setFlashMessages()', () => { - it('sets an array of messages', () => { - const messages: IFlashMessage[] = [ - { type: 'success', message: 'Hello world!!' }, - { type: 'error', message: 'Whoa nelly!', description: 'Uh oh' }, - { type: 'info', message: 'Everything is fine, nothing is ruined' }, - ]; - + describe('messages', () => { + beforeAll(() => { mount(); - FlashMessagesLogic.actions.setFlashMessages(messages); - - expect(FlashMessagesLogic.values.messages).toEqual(messages); }); - it('automatically converts to an array if a single message obj is passed in', () => { - const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; + describe('setFlashMessages', () => { + it('sets an array of messages', () => { + const messages: IFlashMessage[] = [ + { type: 'success', message: 'Hello world!!' }, + { type: 'error', message: 'Whoa nelly!', description: 'Uh oh' }, + { type: 'info', message: 'Everything is fine, nothing is ruined' }, + ]; - mount(); - FlashMessagesLogic.actions.setFlashMessages(message); + FlashMessagesLogic.actions.setFlashMessages(messages); + + expect(FlashMessagesLogic.values.messages).toEqual(messages); + }); + + it('automatically converts to an array if a single message obj is passed in', () => { + const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; + + FlashMessagesLogic.actions.setFlashMessages(message); - expect(FlashMessagesLogic.values.messages).toEqual([message]); + expect(FlashMessagesLogic.values.messages).toEqual([message]); + }); }); - }); - describe('clearFlashMessages()', () => { - it('sets messages back to an empty array', () => { - mount(); - FlashMessagesLogic.actions.setFlashMessages('test' as any); - FlashMessagesLogic.actions.clearFlashMessages(); + describe('clearFlashMessages', () => { + it('resets messages back to an empty array', () => { + FlashMessagesLogic.actions.clearFlashMessages(); - expect(FlashMessagesLogic.values.messages).toEqual([]); + expect(FlashMessagesLogic.values.messages).toEqual([]); + }); }); }); - describe('setQueuedMessages()', () => { - it('sets an array of messages', () => { - const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; - + describe('queuedMessages', () => { + beforeAll(() => { mount(); - FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); + }); + + describe('setQueuedMessages', () => { + it('sets an array of messages', () => { + const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; - expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); + FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); + }); + }); + + describe('clearQueuedMessages', () => { + it('resets queued messages back to an empty array', () => { + FlashMessagesLogic.actions.clearQueuedMessages(); + + expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); + }); }); }); - describe('clearQueuedMessages()', () => { - it('sets queued messages back to an empty array', () => { + describe('toastMessages', () => { + beforeAll(() => { mount(); - FlashMessagesLogic.actions.setQueuedMessages('test' as any); - FlashMessagesLogic.actions.clearQueuedMessages(); + }); - expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); + describe('addToastMessage', () => { + it('appends a toast message to the current toasts array', () => { + FlashMessagesLogic.actions.addToastMessage({ id: 'hello' }); + FlashMessagesLogic.actions.addToastMessage({ id: 'world' }); + FlashMessagesLogic.actions.addToastMessage({ id: 'lorem ipsum' }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { id: 'hello' }, + { id: 'world' }, + { id: 'lorem ipsum' }, + ]); + }); + }); + + describe('dismissToastMessage', () => { + it('removes a specific toast ID from the current toasts array', () => { + FlashMessagesLogic.actions.dismissToastMessage({ id: 'world' }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { id: 'hello' }, + { id: 'lorem ipsum' }, + ]); + }); + }); + + describe('clearToastMessages', () => { + it('resets toast messages back to an empty array', () => { + FlashMessagesLogic.actions.clearToastMessages(); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([]); + }); }); }); describe('history listener logic', () => { - describe('setHistoryListener()', () => { + describe('setHistoryListener', () => { it('sets the historyListener value', () => { mount(); FlashMessagesLogic.actions.setHistoryListener('test' as any); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 5993e67b28a39..f71897cc5a1d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -7,6 +7,8 @@ import { kea, MakeLogicType } from 'kea'; +import { EuiGlobalToastListToast as IToast } from '@elastic/eui'; + import { KibanaLogic } from '../kibana'; import { IFlashMessage } from './types'; @@ -14,6 +16,7 @@ import { IFlashMessage } from './types'; interface FlashMessagesValues { messages: IFlashMessage[]; queuedMessages: IFlashMessage[]; + toastMessages: IToast[]; historyListener: Function | null; } interface FlashMessagesActions { @@ -21,6 +24,9 @@ interface FlashMessagesActions { clearFlashMessages(): void; setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearQueuedMessages(): void; + addToastMessage(newToast: IToast): { newToast: IToast }; + dismissToastMessage(removedToast: IToast): { removedToast: IToast }; + clearToastMessages(): void; setHistoryListener(historyListener: Function): { historyListener: Function }; } @@ -34,6 +40,9 @@ export const FlashMessagesLogic = kea null, setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), clearQueuedMessages: () => null, + addToastMessage: (newToast) => ({ newToast }), + dismissToastMessage: (removedToast) => ({ removedToast }), + clearToastMessages: () => null, setHistoryListener: (historyListener) => ({ historyListener }), }, reducers: { @@ -51,6 +60,15 @@ export const FlashMessagesLogic = kea [], }, ], + toastMessages: [ + [], + { + addToastMessage: (toasts, { newToast }) => [...toasts, newToast], + dismissToastMessage: (toasts, { removedToast }) => + toasts.filter(({ id }) => id !== removedToast.id), + clearToastMessages: () => [], + }, + ], historyListener: [ null, { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index 40317eb390547..f08ac493f20b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -14,5 +14,7 @@ export { setErrorMessage, setQueuedSuccessMessage, setQueuedErrorMessage, + flashSuccessToast, + flashErrorToast, clearFlashMessages, } from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts index 0261a5556a404..d22be32e038cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts @@ -14,6 +14,8 @@ import { setQueuedSuccessMessage, setQueuedErrorMessage, clearFlashMessages, + flashSuccessToast, + flashErrorToast, } from './set_message_helpers'; describe('Flash Message Helpers', () => { @@ -72,4 +74,82 @@ describe('Flash Message Helpers', () => { expect(FlashMessagesLogic.values.messages).toEqual([]); }); + + describe('toast helpers', () => { + afterEach(() => { + FlashMessagesLogic.actions.clearToastMessages(); + }); + + describe('without optional args', () => { + beforeEach(() => { + jest.spyOn(global.Date, 'now').mockReturnValueOnce(1234567890); + }); + + it('flashSuccessToast', () => { + flashSuccessToast('You did a thing!'); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'success', + iconType: 'check', + title: 'You did a thing!', + id: 'successToast-1234567890', + }, + ]); + }); + + it('flashErrorToast', () => { + flashErrorToast('Something went wrong'); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'danger', + iconType: 'alert', + title: 'Something went wrong', + id: 'errorToast-1234567890', + }, + ]); + }); + }); + + describe('with optional args', () => { + it('flashSuccessToast', () => { + flashSuccessToast('You did a thing!', { + text: '', + toastLifeTimeMs: 50, + id: 'customId', + }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'success', + iconType: 'check', + title: 'You did a thing!', + text: '', + toastLifeTimeMs: 50, + id: 'customId', + }, + ]); + }); + + it('flashErrorToast', () => { + flashErrorToast('Something went wrong', { + text: "Here's some helpful advice on what to do", + toastLifeTimeMs: 50000, + id: 'specificErrorId', + }); + + expect(FlashMessagesLogic.values.toastMessages).toEqual([ + { + color: 'danger', + iconType: 'alert', + title: 'Something went wrong', + text: "Here's some helpful advice on what to do", + toastLifeTimeMs: 50000, + id: 'specificErrorId', + }, + ]); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts index 1f06d8cd95930..37f7256ad44a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { FLASH_MESSAGE_TYPES } from './constants'; import { FlashMessagesLogic } from './flash_messages_logic'; +import { ToastOptions } from './types'; export const setSuccessMessage = (message: string) => { FlashMessagesLogic.actions.setFlashMessages({ @@ -38,3 +40,21 @@ export const setQueuedErrorMessage = (message: string) => { export const clearFlashMessages = () => { FlashMessagesLogic.actions.clearFlashMessages(); }; + +export const flashSuccessToast = (message: string, toastOptions: ToastOptions = {}) => { + FlashMessagesLogic.actions.addToastMessage({ + ...FLASH_MESSAGE_TYPES.success, + ...toastOptions, + title: message, + id: toastOptions?.id || `successToast-${Date.now()}`, + }); +}; + +export const flashErrorToast = (message: string, toastOptions: ToastOptions = {}) => { + FlashMessagesLogic.actions.addToastMessage({ + ...FLASH_MESSAGE_TYPES.error, + ...toastOptions, + title: message, + id: toastOptions?.id || `errorToast-${Date.now()}`, + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts index c1d2f8420198d..4c1b613bbc57f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts @@ -5,10 +5,20 @@ * 2.0. */ -import { ReactNode } from 'react'; +import { ReactNode, ReactChild } from 'react'; + +export type FlashMessageTypes = 'success' | 'info' | 'warning' | 'error'; +export type FlashMessageColors = 'success' | 'primary' | 'warning' | 'danger'; export interface IFlashMessage { - type: 'success' | 'info' | 'warning' | 'error'; + type: FlashMessageTypes; message: ReactNode; description?: ReactNode; } + +// @see EuiGlobalToastListToast for more props +export interface ToastOptions { + text?: ReactChild; // Additional text below the message/title, same as IFlashMessage['description'] + toastLifeTimeMs?: number; // Allows customing per-toast timeout + id?: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 8015d22f7c44a..2bef7d373f160 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -24,6 +24,7 @@ interface KibanaLogicProps { charts: ChartsPluginStart; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; + setChromeIsVisible(isVisible: boolean): void; setDocTitle(title: string): void; renderHeaderActions(HeaderActions: FC): void; } @@ -47,6 +48,7 @@ export const KibanaLogic = kea>({ {}, ], setBreadcrumbs: [props.setBreadcrumbs, {}], + setChromeIsVisible: [props.setChromeIsVisible, {}], setDocTitle: [props.setDocTitle, {}], renderHeaderActions: [props.renderHeaderActions, {}], }), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx index 5699568c40558..f288961b72de4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -73,7 +73,7 @@ export const NotFound: React.FC = ({ product = {}, breadcrumbs }) - + } body={ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index b5d1ebb899ba1..504acf9ae1c6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -41,14 +41,6 @@ describe('AttributeSelector', () => { expect(wrapper.find('[data-test-subj="AttributeSelector"]').exists()).toBe(true); }); - it('renders disabled panel with className', () => { - const wrapper = shallow(); - - expect(wrapper.find('[data-test-subj="AttributeSelector"]').prop('className')).toEqual( - 'euiPanel--disabled' - ); - }); - describe('Auth Providers', () => { const findAuthProvidersSelect = (wrapper: ShallowWrapper) => wrapper.find('[data-test-subj="AuthProviderSelect"]'); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index 48d1447e9bd0f..0417331be208d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -100,11 +100,7 @@ export const AttributeSelector: React.FC = ({ handleAuthProviderChange = () => null, }) => { return ( - +

{ATTRIBUTE_SELECTOR_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 5589309d00ef8..e1c43dca581fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -5,14 +5,14 @@ * 2.0. */ +import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; + import React from 'react'; import { shallow } from 'enzyme'; import { EuiFieldSearch, EuiTableRow } from '@elastic/eui'; -import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; - import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; import { RoleMappingsTable } from './role_mappings_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx index 2140b3392abae..b4108e584086d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx @@ -50,7 +50,7 @@ export const SetupGuideLayout: React.FC = ({ }) => { const { cloud } = useValues(KibanaLogic); const isCloudEnabled = Boolean(cloud.isCloudEnabled); - const cloudDeploymentLink = cloud.cloudDeploymentUrl || ''; + const cloudDeploymentLink = cloud.deploymentUrl || ''; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index 5d15196fba5a6..f9679bd42c07d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -7,10 +7,6 @@ import React from 'react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import _kebabCase from 'lodash/kebabCase'; - import { EuiFlexGroup, EuiFlexItem, @@ -72,7 +68,7 @@ export const SourceRow: React.FC = ({ const fixLink = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 48bdcd6551b65..a2c0ec18def4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -57,11 +57,13 @@ describe('WorkplaceSearchConfigured', () => { setMockActions({ initializeAppData, setContext }); }); - it('renders layout and header actions', () => { + it('renders layout, chrome, and header actions', () => { const wrapper = shallow(); expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(OverviewMVP)).toHaveLength(1); + + expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); expect(mockKibanaValues.renderHeaderActions).toHaveBeenCalledWith(WorkplaceSearchHeaderActions); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index c269a987dc092..7a76de43be41b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -53,7 +53,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); - const { renderHeaderActions } = useValues(KibanaLogic); + const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); const { pathname } = useLocation(); @@ -66,11 +66,13 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + useEffect(() => { + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. - const isOrganization = !pathname.match(personalSourceUrlRegex); - setContext(isOrganization); + setContext(isOrganization); + setChromeIsVisible(isOrganization); + }, [pathname]); useEffect(() => { if (!hasInitialized) { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index 41f53523bca4e..0ee872f7cfe8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -22,7 +22,7 @@ import { ConfigurationIntro } from './configuration_intro'; import { ConfigureCustom } from './configure_custom'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; -import { ReAuthenticate } from './re_authenticate'; +import { Reauthenticate } from './reauthenticate'; import { SaveConfig } from './save_config'; import { SaveCustom } from './save_custom'; @@ -142,13 +142,13 @@ describe('AddSourceList', () => { expect(wrapper.find(SaveCustom)).toHaveLength(1); }); - it('renders ReAuthenticate step', () => { + it('renders Reauthenticate step', () => { setMockValues({ ...mockValues, - addSourceCurrentStep: AddSourceSteps.ReAuthenticateStep, + addSourceCurrentStep: AddSourceSteps.ReauthenticateStep, }); const wrapper = shallow(); - expect(wrapper.find(ReAuthenticate)).toHaveLength(1); + expect(wrapper.find(Reauthenticate)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 30f5009ac0b3c..8186c43efef49 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -27,7 +27,7 @@ import { ConfigurationIntro } from './configuration_intro'; import { ConfigureCustom } from './configure_custom'; import { ConfigureOauth } from './configure_oauth'; import { ConnectInstance } from './connect_instance'; -import { ReAuthenticate } from './re_authenticate'; +import { Reauthenticate } from './reauthenticate'; import { SaveConfig } from './save_config'; import { SaveCustom } from './save_custom'; @@ -150,8 +150,8 @@ export const AddSource: React.FC = (props) => { header={header} /> )} - {addSourceCurrentStep === AddSourceSteps.ReAuthenticateStep && ( - + {addSourceCurrentStep === AddSourceSteps.ReauthenticateStep && ( + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 8ced90e7d7729..b52b354a6b115 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -276,7 +276,7 @@ describe('AddSourceLogic', () => { const addSourceProps = { sourceIndex: 1, reAuthenticate: true }; AddSourceLogic.actions.initializeAddSource(addSourceProps); - expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReAuthenticateStep); + expect(setAddSourceStepSpy).toHaveBeenCalledWith(AddSourceSteps.ReauthenticateStep); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 6ca7f6fa72e24..0bd37aed81c32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -42,7 +42,7 @@ export enum AddSourceSteps { ConfigureCustomStep = 'Configure Custom', ConfigureOauthStep = 'Configure Oauth', SaveCustomStep = 'Save Custom', - ReAuthenticateStep = 'ReAuthenticate', + ReauthenticateStep = 'Reauthenticate', } export interface OauthParams { @@ -577,6 +577,6 @@ const getFirstStep = (props: AddSourceProps): AddSourceSteps => { if (isCustom) return AddSourceSteps.ConfigureCustomStep; if (connect) return AddSourceSteps.ConnectInstanceStep; if (configure) return AddSourceSteps.ConfigureOauthStep; - if (reAuthenticate) return AddSourceSteps.ReAuthenticateStep; + if (reAuthenticate) return AddSourceSteps.ReauthenticateStep; return AddSourceSteps.ConfigIntroStep; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.test.tsx similarity index 87% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.test.tsx index 38b6925008181..c38ab167b18de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.test.tsx @@ -12,9 +12,9 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ReAuthenticate } from './re_authenticate'; +import { Reauthenticate } from './reauthenticate'; -describe('ReAuthenticate', () => { +describe('Reauthenticate', () => { // Needed to mock redirect window.location.replace(oauthUrl) const mockReplace = jest.fn(); const mockWindow = { @@ -44,14 +44,14 @@ describe('ReAuthenticate', () => { }); it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('form')).toHaveLength(1); }); it('handles form submission', () => { jest.spyOn(window.location, 'replace').mockImplementationOnce(mockReplace); - const wrapper = shallow(); + const wrapper = shallow(); const preventDefault = jest.fn(); wrapper.find('form').simulate('submit', { preventDefault }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.tsx similarity index 91% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.tsx index f57118b952eac..fa604ef758a44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/reauthenticate.tsx @@ -22,12 +22,12 @@ interface SourceQueryParams { sourceId: string; } -interface ReAuthenticateProps { +interface ReauthenticateProps { name: string; header: React.ReactNode; } -export const ReAuthenticate: React.FC = ({ name, header }) => { +export const Reauthenticate: React.FC = ({ name, header }) => { const { search } = useLocation() as Location; const { sourceId } = (parseQueryParams(search) as unknown) as SourceQueryParams; @@ -66,7 +66,7 @@ export const ReAuthenticate: React.FC = ({ name, header }) 'xpack.enterpriseSearch.workplaceSearch.contentSource.reAuthenticate.body', { defaultMessage: - 'Your {name} credentials are no longer valid. Please re-authenticate with the original credentials to resume content syncing.', + 'Your {name} credentials are no longer valid. Please reauthenticate with the original credentials to resume content syncing.', values: { name }, } )} @@ -79,7 +79,7 @@ export const ReAuthenticate: React.FC = ({ name, header }) {i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.reAuthenticate.button', { - defaultMessage: 'Re-authenticate {name}', + defaultMessage: 'Reauthenticate {name}', values: { name }, } )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 99cebd5ded585..bf0c5471f7b57 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -33,7 +33,7 @@ export const SourceSubNav: React.FC = () => { const isCustom = serviceType === CUSTOM_SERVICE_TYPE; return ( - <> +
{NAV.OVERVIEW} @@ -53,6 +53,6 @@ export const SourceSubNav: React.FC = () => { {NAV.SETTINGS} - +
); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx index 488eb4b49853b..9e3b50ea083eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -17,6 +17,8 @@ import { EuiCallOut } from '@elastic/eui'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_CAN_CREATE_PAGE_TITLE, PRIVATE_VIEW_ONLY_PAGE_TITLE, @@ -40,6 +42,7 @@ describe('PrivateSourcesLayout', () => { const wrapper = shallow({children}); expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + expect(wrapper.find(SourceSubNav)).toHaveLength(1); }); it('uses correct title and description when private sources are enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx index bdc2421432c8a..2a6281075dc40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.tsx @@ -14,6 +14,8 @@ import { EuiPage, EuiPageSideBar, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { SourceSubNav } from './components/source_sub_nav'; + import { PRIVATE_DASHBOARD_READ_ONLY_MODE_WARNING, PRIVATE_CAN_CREATE_PAGE_TITLE, @@ -49,6 +51,7 @@ export const PrivateSourcesLayout: React.FC = ({ + {readOnlyMode && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index d20d0576d11ce..a9712cc4e1dc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -214,7 +214,7 @@ describe('SourceLogic', () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); expect(http.get).toHaveBeenCalledWith( - '/api/workplace_search/org/sources/123/federated_summary' + '/api/workplace_search/account/sources/123/federated_summary' ); await promise; expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 72700ce42c75d..3da90c4fc7739 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -156,7 +156,7 @@ export const SourceLogic = kea>({ } }, initializeFederatedSummary: async ({ sourceId }) => { - const route = `/api/workplace_search/org/sources/${sourceId}/federated_summary`; + const route = `/api/workplace_search/account/sources/${sourceId}/federated_summary`; try { const response = await HttpLogic.values.http.get(route); actions.onUpdateSummary(response.summary); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss index f142567fb621f..abab139e32369 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources.scss @@ -30,3 +30,9 @@ margin-left: -$sideBarWidth; } } + +.sourcesSubNav { + li { + display: block; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index f4a56c8a0beaa..84bff65e62cef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -82,7 +82,7 @@ export const SourcesRouter: React.FC = () => { ))} {staticSourceData.map(({ addPath, name }, i) => ( - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts index 836efa82995fc..9f12e8f202d50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -12,12 +12,12 @@ import { mockHttpValues, } from '../../../__mocks__'; import { groups } from '../../__mocks__/groups.mock'; +import { mockGroupValues } from './__mocks__/group_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { GROUPS_PATH } from '../../routes'; -import { mockGroupValues } from './__mocks__/group_logic.mock'; import { GroupLogic } from './group_logic'; describe('GroupLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index 8470c5d3e0f66..54f8580a8eab9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -98,7 +98,7 @@ describe('GroupOverview', () => { messages: [mockSuccessMessage], }); const wrapper = shallow(); - const flashMessages = wrapper.find(FlashMessages).dive().shallow(); + const flashMessages = wrapper.find(FlashMessages).dive().childAt(0).dive(); expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index 806c6e1c69f84..bb6e7c0c76faf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -9,13 +9,13 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../ import { contentSources } from '../../__mocks__/content_sources.mock'; import { groups } from '../../__mocks__/groups.mock'; import { users } from '../../__mocks__/users.mock'; +import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { JSON_HEADER as headers } from '../../../../../common/constants'; import { DEFAULT_META } from '../../../shared/constants'; -import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { GroupsLogic } from './groups_logic'; // We need to mock out the debounced functionality diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 7a368e7d384ea..5059533519a6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { setMockValues } from './__mocks__'; import { OnboardingCard } from './onboarding_card'; import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index 412977f18fadf..110557ac4087a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -13,7 +14,6 @@ import { shallow } from 'enzyme'; import { EuiFlexGrid } from '@elastic/eui'; -import { setMockValues } from './__mocks__'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index 2ec2d949ff491..19c893bec81ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -7,6 +7,7 @@ import '../../../__mocks__/react_router_history.mock'; import './__mocks__/overview_logic.mock'; +import { mockActions, setMockValues } from './__mocks__'; import React from 'react'; @@ -15,7 +16,6 @@ import { shallow, mount } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { mockActions, setMockValues } from './__mocks__'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; import { Overview } from './overview'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 0e84315104343..75a41216ffbb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -6,8 +6,8 @@ */ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; - import { mockOverviewValues } from './__mocks__'; + import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 9ab7b908ad3cd..3a925f011cc18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -18,7 +18,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { setMockValues } from './__mocks__'; import { RecentActivity, RecentActivityItem } from './recent_activity'; const organization = { name: 'foo', defaultOrgName: 'bar' }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx index 7a368e7d384ea..5059533519a6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/onboarding_steps.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -15,7 +15,6 @@ import { shallow } from 'enzyme'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { setMockValues } from './__mocks__'; import { OnboardingCard } from './onboarding_card'; import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx index 412977f18fadf..110557ac4087a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/organization_stats.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -13,7 +14,6 @@ import { shallow } from 'enzyme'; import { EuiFlexGrid } from '@elastic/eui'; -import { setMockValues } from './__mocks__'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx index 2ec2d949ff491..19c893bec81ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview.test.tsx @@ -7,6 +7,7 @@ import '../../../__mocks__/react_router_history.mock'; import './__mocks__/overview_logic.mock'; +import { mockActions, setMockValues } from './__mocks__'; import React from 'react'; @@ -15,7 +16,6 @@ import { shallow, mount } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { mockActions, setMockValues } from './__mocks__'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; import { Overview } from './overview'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts index 0e84315104343..75a41216ffbb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/overview_logic.test.ts @@ -6,8 +6,8 @@ */ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; - import { mockOverviewValues } from './__mocks__'; + import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx index 0b62207afc520..7213526c8864a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview_mvp/recent_activity.test.tsx @@ -6,7 +6,7 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; - +import { setMockValues } from './__mocks__'; import './__mocks__/overview_logic.mock'; import React from 'react'; @@ -16,7 +16,6 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { setMockValues } from './__mocks__'; import { RecentActivity, RecentActivityItem } from './recent_activity'; const organization = { name: 'foo', defaultOrgName: 'bar' }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index cf402f4525f9e..7db1e82d29449 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -141,7 +141,7 @@ export const RoleMapping: React.FC = ({ isNew }) => { - +

{ROLE_LABEL}

@@ -158,7 +158,7 @@ export const RoleMapping: React.FC = ({ isNew }) => {
- +

{GROUP_ASSIGNMENT_TITLE}

diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index f00e81a5accf7..dd1a62d243d03 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -114,6 +114,9 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + // The Workplace Search Personal dashboard needs the chrome hidden. We hide it globally + // here first to prevent a flash of chrome on the Personal dashboard and unhide it for admin routes. + chrome.setIsVisible(false); await this.getInitialData(http); const pluginData = this.getPluginData(); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts new file mode 100644 index 0000000000000..3152b371c2fbb --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/api_logs.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerApiLogsRoutes } from './api_logs'; + +describe('API logs routes', () => { + describe('GET /api/app_search/engines/{engineName}/api_logs', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/api_logs', + }); + + registerApiLogsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/api_logs/collection', + }); + }); + + describe('validates', () => { + it('with required query params', () => { + const request = { + query: { + 'filters[date][from]': '1970-01-01T12:00:00.000Z', + 'filters[date][to]': '1970-01-02T12:00:00.000Z', + 'page[current]': 1, + sort_direction: 'desc', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/api_logs.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/api_logs.ts new file mode 100644 index 0000000000000..d57ecb29294be --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/api_logs.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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerApiLogsRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/api_logs', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + query: schema.object({ + 'filters[date][from]': schema.string(), // Date string, expected format: ISO string + 'filters[date][to]': schema.string(), // Date string, expected format: ISO string + 'page[current]': schema.number(), + sort_direction: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/api_logs/collection', + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 3c8501ec15b3d..1d48614e73374 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -8,6 +8,7 @@ import { RouteDependencies } from '../../plugin'; import { registerAnalyticsRoutes } from './analytics'; +import { registerApiLogsRoutes } from './api_logs'; import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; @@ -29,5 +30,6 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerSearchSettingsRoutes(dependencies); registerRoleMappingsRoutes(dependencies); registerResultSettingsRoutes(dependencies); + registerApiLogsRoutes(dependencies); registerOnboardingRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts index 8d1a7e3ead37b..e38380d60c6e9 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.test.ts @@ -88,4 +88,48 @@ describe('result settings routes', () => { }); }); }); + + describe('POST /api/app_search/engines/{name}/sample_response_search', () => { + const mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/sample_response_search', + }); + + beforeEach(() => { + registerResultSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + body: { + query: 'test', + result_fields: resultFields, + }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/sample_response_search', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + query: 'test', + result_fields: resultFields, + }, + }; + mockRouter.shouldValidate(request); + }); + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts index 38cb4aa922738..b091ae7a539c2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/result_settings.ts @@ -45,4 +45,22 @@ export function registerResultSettingsRoutes({ path: '/as/engines/:engineName/result_settings', }) ); + + router.post( + { + path: '/api/app_search/engines/{engineName}/sample_response_search', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + query: schema.string(), + result_fields: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/sample_response_search', + }) + ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 8257dd0dc52b0..1dd6d859d88ad 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -22,9 +22,9 @@ const schemaValuesSchema = schema.recordOf( ); const pageSchema = schema.object({ - current: schema.number(), - size: schema.number(), - total_pages: schema.number(), + current: schema.nullable(schema.number()), + size: schema.nullable(schema.number()), + total_pages: schema.nullable(schema.number()), total_results: schema.number(), }); diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index 0fc59e2b525a8..11cf4ac3615bf 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { ES_FIELD_TYPES } from '../../../../src/plugins/data/common'; export interface HasImportPermission { @@ -83,7 +84,9 @@ export interface ImportResponse { pipelineId?: string; docCount: number; failures: ImportFailure[]; - error?: any; + error?: { + error: estypes.ErrorCause; + }; ingestError?: boolean; } diff --git a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx index 29aed0cd52f7e..a3bc2ed082b1a 100644 --- a/x-pack/plugins/file_upload/public/components/import_complete_view.tsx +++ b/x-pack/plugins/file_upload/public/components/import_complete_view.tsx @@ -7,19 +7,20 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonIcon, EuiCallOut, EuiCopy, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { CodeEditor, KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { getHttp, getUiSettings } from '../kibana_services'; +import { getDocLinks, getHttp, getUiSettings } from '../kibana_services'; import { ImportResults } from '../importer'; const services = { @@ -27,8 +28,10 @@ const services = { }; interface Props { + failedPermissionCheck: boolean; importResults?: ImportResults; indexPatternResp?: object; + indexName: string; } export class ImportCompleteView extends Component { @@ -57,9 +60,12 @@ export class ImportCompleteView extends Component { iconType="copy" color="text" data-test-subj={copyButtonDataTestSubj} - aria-label={i18n.translate('xpack.fileUpload.copyButtonAriaLabel', { - defaultMessage: 'Copy to clipboard', - })} + aria-label={i18n.translate( + 'xpack.fileUpload.importComplete.copyButtonAriaLabel', + { + defaultMessage: 'Copy to clipboard', + } + )} /> )} @@ -90,21 +96,65 @@ export class ImportCompleteView extends Component { } _getStatusMsg() { + if (this.props.failedPermissionCheck) { + return ( + +

+ {i18n.translate('xpack.fileUpload.importComplete.permissionFailureMsg', { + defaultMessage: + 'You do not have permission to create or import data into index "{indexName}".', + values: { indexName: this.props.indexName }, + })} +

+ + {i18n.translate('xpack.fileUpload.importComplete.permission.docLink', { + defaultMessage: 'View file import permissions', + })} + +
+ ); + } + if (!this.props.importResults || !this.props.importResults.success) { - return i18n.translate('xpack.fileUpload.uploadFailureMsg', { - defaultMessage: 'File upload failed.', - }); + const errorMsg = + this.props.importResults && this.props.importResults.error + ? i18n.translate('xpack.fileUpload.importComplete.uploadFailureMsgErrorBlock', { + defaultMessage: 'Error: {reason}', + values: { reason: this.props.importResults.error.error.reason }, + }) + : ''; + return ( + +

{errorMsg}

+
+ ); } - const successMsg = i18n.translate('xpack.fileUpload.uploadSuccessMsg', { - defaultMessage: 'File upload complete: indexed {numFeatures} features.', + const successMsg = i18n.translate('xpack.fileUpload.importComplete.uploadSuccessMsg', { + defaultMessage: 'Indexed {numFeatures} features.', values: { numFeatures: this.props.importResults.docCount, }, }); const failedFeaturesMsg = this.props.importResults.failures?.length - ? i18n.translate('xpack.fileUpload.failedFeaturesMsg', { + ? i18n.translate('xpack.fileUpload.importComplete.failedFeaturesMsg', { defaultMessage: 'Unable to index {numFailures} features.', values: { numFailures: this.props.importResults.failures.length, @@ -112,47 +162,60 @@ export class ImportCompleteView extends Component { }) : ''; - return `${successMsg} ${failedFeaturesMsg}`; + return ( + +

{`${successMsg} ${failedFeaturesMsg}`}

+
+ ); + } + + _renderIndexManagementMsg() { + return this.props.importResults && this.props.importResults.success ? ( + +

+ + + + +

+
+ ) : null; } render() { return ( - -

{this._getStatusMsg()}

-
+ {this._getStatusMsg()} + {this._renderCodeEditor( this.props.importResults, - i18n.translate('xpack.fileUpload.jsonImport.indexingResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexingResponse', { defaultMessage: 'Import response', }), 'indexRespCopyButton' )} {this._renderCodeEditor( this.props.indexPatternResp, - i18n.translate('xpack.fileUpload.jsonImport.indexPatternResponse', { + i18n.translate('xpack.fileUpload.importComplete.indexPatternResponse', { defaultMessage: 'Index pattern response', }), 'indexPatternRespCopyButton' )} - -
- - - - -
-
+ {this._renderIndexManagementMsg()}
); } diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx index 371d68443bc2c..d73c6e9c5fb3a 100644 --- a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx +++ b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.tsx @@ -16,6 +16,7 @@ import { FileUploadComponentProps } from '../lazy_load_bundle'; import { ImportResults } from '../importer'; import { GeoJsonImporter } from '../importer/geojson_importer'; import { Settings } from '../../common'; +import { hasImportPermission } from '../api'; enum PHASE { CONFIGURE = 'CONFIGURE', @@ -31,6 +32,7 @@ function getWritingToIndexMsg(progress: number) { } interface State { + failedPermissionCheck: boolean; geoFieldType: ES_FIELD_TYPES.GEO_POINT | ES_FIELD_TYPES.GEO_SHAPE; importStatus: string; importResults?: ImportResults; @@ -45,6 +47,7 @@ export class JsonUploadAndParse extends Component ); } diff --git a/x-pack/plugins/file_upload/public/kibana_services.ts b/x-pack/plugins/file_upload/public/kibana_services.ts index a604136ca34e4..dfe2785e7a2bc 100644 --- a/x-pack/plugins/file_upload/public/kibana_services.ts +++ b/x-pack/plugins/file_upload/public/kibana_services.ts @@ -15,6 +15,7 @@ export function setStartServices(core: CoreStart, plugins: FileUploadStartDepend pluginsStart = plugins; } +export const getDocLinks = () => coreStart.docLinks; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getHttp = () => coreStart.http; export const getSavedObjectsClient = () => coreStart.savedObjects.client; diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index de16f6555d4bd..0eb392e784334 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -13,10 +13,11 @@ import * as AgentService from '../services/agents'; import { isFleetServerSetup } from '../services/fleet_server'; export interface AgentUsage { - total: number; - online: number; - error: number; + total_enrolled: number; + healthy: number; + unhealthy: number; offline: number; + total_all_statuses: number; } export const getAgentUsage = async ( @@ -27,21 +28,26 @@ export const getAgentUsage = async ( // TODO: unsure if this case is possible at all. if (!soClient || !esClient || !(await isFleetServerSetup())) { return { - total: 0, - online: 0, - error: 0, + total_enrolled: 0, + healthy: 0, + unhealthy: 0, offline: 0, + total_all_statuses: 0, }; } - const { total, online, error, offline } = await AgentService.getAgentStatusForAgentPolicy( - soClient, - esClient - ); - return { + const { total, + inactive, online, error, offline, + } = await AgentService.getAgentStatusForAgentPolicy(soClient, esClient); + return { + total_enrolled: total, + healthy: online, + unhealthy: error, + offline, + total_all_statuses: total + inactive, }; }; diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts index 7992d54d1dfad..842bb95fe813f 100644 --- a/x-pack/plugins/fleet/server/collectors/register.ts +++ b/x-pack/plugins/fleet/server/collectors/register.ts @@ -49,10 +49,36 @@ export function registerFleetUsageCollector( schema: { agents_enabled: { type: 'boolean' }, agents: { - total: { type: 'long' }, - online: { type: 'long' }, - error: { type: 'long' }, - offline: { type: 'long' }, + total_enrolled: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents, in any state', + }, + }, + healthy: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents in a healthy state', + }, + }, + unhealthy: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents in an unhealthy state', + }, + }, + offline: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents currently offline', + }, + }, + total_all_statuses: { + type: 'long', + _meta: { + description: 'The total number of agents in any state, both enrolled and inactive', + }, + }, }, packages: { type: 'array', diff --git a/x-pack/plugins/fleet/server/errors/utils.ts b/x-pack/plugins/fleet/server/errors/utils.ts index 6d7d4aaffa2a3..2eae04e05bd6b 100644 --- a/x-pack/plugins/fleet/server/errors/utils.ts +++ b/x-pack/plugins/fleet/server/errors/utils.ts @@ -10,3 +10,7 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; export function isESClientError(error: unknown): error is ResponseError { return error instanceof ResponseError; } + +export const isElasticsearchVersionConflictError = (error: Error): boolean => { + return isESClientError(error) && error.meta.statusCode === 409; +}; diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index f3fb01655974e..737b6874a8133 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -55,17 +55,18 @@ export async function getAgentStatusForAgentPolicy( agentPolicyId?: string, filterKuery?: string ) { - const [all, online, error, offline, updating] = await pMap( + const [all, allActive, online, error, offline, updating] = await pMap( [ - undefined, + undefined, // All agents, including inactive + undefined, // All active agents AgentStatusKueryHelper.buildKueryForOnlineAgents(), AgentStatusKueryHelper.buildKueryForErrorAgents(), AgentStatusKueryHelper.buildKueryForOfflineAgents(), AgentStatusKueryHelper.buildKueryForUpdatingAgents(), ], - (kuery) => + (kuery, index) => getAgentsByKuery(esClient, { - showInactive: false, + showInactive: index === 0, perPage: 0, page: 1, kuery: joinKuerys( @@ -84,7 +85,8 @@ export async function getAgentStatusForAgentPolicy( return { events: await getEventsCount(soClient, agentPolicyId), - total: all.total, + total: allActive.total, + inactive: all.total - allActive.total, online: online.total, error: error.total, offline: offline.total, diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index ff243eff11570..59ec3a0a63206 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -102,7 +102,7 @@ export async function unenrollAgents( // Invalidate all API keys if (apiKeys.length) { - await APIKeyService.invalidateAPIKeys(soClient, apiKeys); + await APIKeyService.invalidateAPIKeys(apiKeys); } } else { // Create unenroll action for each agent @@ -152,10 +152,10 @@ export async function forceUnenrollAgent( await Promise.all([ agent.access_api_key_id - ? APIKeyService.invalidateAPIKeys(soClient, [agent.access_api_key_id]) + ? APIKeyService.invalidateAPIKeys([agent.access_api_key_id]) : undefined, agent.default_api_key_id - ? APIKeyService.invalidateAPIKeys(soClient, [agent.default_api_key_id]) + ? APIKeyService.invalidateAPIKeys([agent.default_api_key_id]) : undefined, ]); diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index b3edb20d51c4f..643caa8d3bb6f 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -86,7 +86,7 @@ export async function deleteEnrollmentApiKey( ) { const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id); - await invalidateAPIKeys(soClient, [enrollmentApiKey.api_key_id]); + await invalidateAPIKeys([enrollmentApiKey.api_key_id]); await esClient.update({ index: ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/plugins/fleet/server/services/api_keys/security.ts b/x-pack/plugins/fleet/server/services/api_keys/security.ts index 599785cb5ff7b..e68bc406055b0 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/security.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/security.ts @@ -56,30 +56,14 @@ export async function createAPIKey( } } -export async function invalidateAPIKeys(soClient: SavedObjectsClientContract, ids: string[]) { - const adminUser = await outputService.getAdminUser(soClient); - if (!adminUser) { - throw new Error('No admin user configured'); - } - const request = KibanaRequest.from(({ - path: '/', - route: { settings: {} }, - url: { href: '/' }, - raw: { req: { url: '/' } }, - headers: { - authorization: `Basic ${Buffer.from(`${adminUser.username}:${adminUser.password}`).toString( - 'base64' - )}`, - }, - } as unknown) as Request); - +export async function invalidateAPIKeys(ids: string[]) { const security = appContextService.getSecurity(); if (!security) { throw new Error('Missing security plugin'); } try { - const res = await security.authc.apiKeys.invalidate(request, { + const res = await security.authc.apiKeys.invalidateAsInternalUser({ ids, }); diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts index 07232e66b4467..d4f129a1ae241 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts @@ -9,6 +9,8 @@ import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import type { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; + import { FLEET_SERVER_ARTIFACTS_INDEX } from '../../../common'; import { ArtifactsElasticsearchError } from '../../errors'; @@ -85,7 +87,7 @@ describe('When using the artifacts services', () => { expect(esClientMock.create).toHaveBeenCalledWith({ index: FLEET_SERVER_ARTIFACTS_INDEX, - id: expect.any(String), + id: `${artifact.packageName}:${artifact.identifier}-${artifact.decodedSha256}`, body: { ...newArtifactToElasticsearchProperties(newArtifact), created: expect.any(String), @@ -100,6 +102,14 @@ describe('When using the artifacts services', () => { }); }); + it('should ignore 409 errors from elasticsearch', async () => { + const error = new ResponseError({ statusCode: 409 } as ApiResponse); + // Unclear why `mockRejectedValue()` has the params value type set to `never` + // @ts-expect-error + esClientMock.create.mockRejectedValue(error); + await expect(() => createArtifact(esClientMock, newArtifact)).not.toThrow(); + }); + it('should throw an ArtifactElasticsearchError if one is encountered', async () => { setEsClientMethodResponseToError(esClientMock, 'create'); await expect(createArtifact(esClientMock, newArtifact)).rejects.toBeInstanceOf( diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts index 77785aeb026c1..6e2c22cc2f045 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts @@ -11,7 +11,6 @@ import { promisify } from 'util'; import type { BinaryLike } from 'crypto'; import { createHash } from 'crypto'; -import uuid from 'uuid'; import type { ElasticsearchClient } from 'kibana/server'; import type { ListResult } from '../../../common'; @@ -19,6 +18,8 @@ import { FLEET_SERVER_ARTIFACTS_INDEX } from '../../../common'; import { ArtifactsElasticsearchError } from '../../errors'; +import { isElasticsearchVersionConflictError } from '../../errors/utils'; + import { isElasticsearchItemNotFoundError } from './utils'; import type { Artifact, @@ -28,7 +29,11 @@ import type { ListArtifactsProps, NewArtifact, } from './types'; -import { esSearchHitToArtifact, newArtifactToElasticsearchProperties } from './mappings'; +import { + esSearchHitToArtifact, + newArtifactToElasticsearchProperties, + uniqueIdFromArtifact, +} from './mappings'; const deflateAsync = promisify(deflate); @@ -57,7 +62,7 @@ export const createArtifact = async ( esClient: ElasticsearchClient, artifact: NewArtifact ): Promise => { - const id = uuid.v4(); + const id = uniqueIdFromArtifact(artifact); const newArtifactData = newArtifactToElasticsearchProperties(artifact); try { @@ -67,11 +72,14 @@ export const createArtifact = async ( body: newArtifactData, refresh: 'wait_for', }); - - return esSearchHitToArtifact({ _id: id, _source: newArtifactData }); } catch (e) { - throw new ArtifactsElasticsearchError(e); + // we ignore 409 errors from the create (document already exists) + if (!isElasticsearchVersionConflictError(e)) { + throw new ArtifactsElasticsearchError(e); + } } + + return esSearchHitToArtifact({ _id: id, _source: newArtifactData }); }; export const deleteArtifact = async (esClient: ElasticsearchClient, id: string): Promise => { diff --git a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts index 3b81e47577ff7..79d14a27fa827 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts @@ -72,3 +72,13 @@ export const relativeDownloadUrlFromArtifact = < decodedSha256 ); }; + +export const uniqueIdFromArtifact = < + T extends Pick +>({ + identifier, + decodedSha256, + packageName, +}: T): string => { + return `${packageName}:${identifier}-${decodedSha256}`; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index dde9f1733dfe3..bc4ffffb68358 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -65,6 +65,12 @@ custom: {{ custom }} {{#if key.patterns}} key.patterns: {{key.patterns}} {{/if}} +{{#if emptyfield}} +emptyfield: {{emptyfield}} +{{/if}} +{{#if nullfield}} +nullfield: {{nullfield}} +{{/if}} {{ testEmpty }} `; const vars = { @@ -82,6 +88,8 @@ foo: bar `, }, password: { type: 'password', value: '' }, + emptyfield: { type: 'yaml', value: '' }, + nullfield: { type: 'yaml' }, }; const output = compileTemplate(vars, streamTemplate); diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 26e1497e93852..84a8ab581354a 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -90,7 +90,7 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt if (recordEntry.type && recordEntry.type === 'yaml') { const yamlKeyPlaceholder = `##${key}##`; - varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; + varPart[lastKeyPart] = recordEntry.value ? `"${yamlKeyPlaceholder}"` : null; yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; } else if ( recordEntry.type && diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts index 95072d3ae2e2c..b0dce60085529 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts @@ -116,6 +116,11 @@ async function createIndex(esClient: ElasticsearchClient, indexName: string, ind index: indexName, body: { ...indexData, + settings: { + ...(indexData.settings || {}), + auto_expand_replicas: '0-1', + }, + mappings: Object.assign({ ...indexData.mappings, _meta: { ...(indexData.mappings._meta || {}), migrationHash }, diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json index 3008ee74ab50c..94ad02c6d5f18 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json @@ -24,6 +24,9 @@ }, "type": { "type": "keyword" + }, + "user_id" : { + "type": "keyword" } } } diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json index 58ae1a2e00ea4..32caa684679d8 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_agents.json @@ -7,7 +7,8 @@ "type": "keyword" }, "action_seq_no": { - "type": "integer" + "type": "integer", + "index": false }, "active": { "type": "boolean" diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index f078b214e4dfd..78172e4dae366 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -25,6 +25,7 @@ import { listEnrollmentApiKeys, getEnrollmentAPIKey } from '../api_keys/enrollme import { appContextService } from '../app_context'; import { isAgentsSetup } from '../agents'; import { agentPolicyService } from '../agent_policy'; +import { invalidateAPIKeys } from '../api_keys'; export async function runFleetServerMigration() { // If Agents are not setup skip as there is nothing to migrate @@ -56,6 +57,7 @@ function getInternalUserSOClient() { async function migrateAgents() { const esClient = appContextService.getInternalUserESClient(); const soClient = getInternalUserSOClient(); + const logger = appContextService.getLogger(); let hasMore = true; while (hasMore) { const res = await soClient.find({ @@ -75,11 +77,19 @@ async function migrateAgents() { .getEncryptedSavedObjects() .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, so.id); + await invalidateAPIKeys( + [attributes.access_api_key_id, attributes.default_api_key_id].filter( + (keyId): keyId is string => keyId !== undefined + ) + ).catch((error) => { + logger.error(`Invalidating API keys for agent ${so.id} failed: ${error.message}`); + }); + const body: FleetServerAgent = { type: attributes.type, - active: attributes.active, + active: false, enrolled_at: attributes.enrolled_at, - unenrolled_at: attributes.unenrolled_at, + unenrolled_at: new Date().toISOString(), unenrollment_started_at: attributes.unenrollment_started_at, upgraded_at: attributes.upgraded_at, upgrade_started_at: attributes.upgrade_started_at, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 2b5319cafb1d4..122fb83edab45 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -247,25 +247,12 @@ export const setup = async (arg?: { }; }; - const createToggleDeletePhaseActions = () => { - const enablePhase = async () => { - await act(async () => { - find('enableDeletePhaseButton').simulate('click'); - }); - component.update(); - }; - - const disablePhase = async () => { - await act(async () => { - find('disableDeletePhaseButton').simulate('click'); - }); - component.update(); - }; - - return { - enablePhase, - disablePhase, - }; + const enableDeletePhase = async (isEnabled: boolean) => { + const buttonSelector = isEnabled ? 'enableDeletePhaseButton' : 'disableDeletePhaseButton'; + await act(async () => { + find(buttonSelector).simulate('click'); + }); + component.update(); }; const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); @@ -406,7 +393,7 @@ export const setup = async (arg?: { }, delete: { isShown: () => exists('delete-phaseContent'), - ...createToggleDeletePhaseActions(), + enable: enableDeletePhase, ...createMinAgeActions('delete'), }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts index 0fb4951e4a4a6..0d49024ac6d67 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts @@ -50,7 +50,7 @@ describe(' delete phase', () => { component.update(); expect(actions.delete.isShown()).toBeFalsy(); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.delete.isShown()).toBeTruthy(); }); @@ -65,7 +65,7 @@ describe(' delete phase', () => { component.update(); expect(actions.delete.hasMinAgeInput()).toBeFalsy(); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.delete.hasMinAgeInput()).toBeTruthy(); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts index 506ac4cece032..ad4d67826b1ed 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -11,7 +11,7 @@ import { getDefaultHotPhasePolicy } from '../constants'; import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -describe(' timeline', () => { +describe(' rollover', () => { let testBed: EditPolicyTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -92,7 +92,7 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); await actions.frozen.enable(true); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy(); @@ -108,7 +108,7 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); await actions.frozen.enable(true); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts index 3618bad45e4f1..a6cce7fdfaca0 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts @@ -55,7 +55,7 @@ describe(' timeline', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(false); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.timeline.hasHotPhase()).toBe(true); expect(actions.timeline.hasWarmPhase()).toBe(true); expect(actions.timeline.hasColdPhase()).toBe(true); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts index c5c4bb1be87e0..455a7bd442167 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -47,38 +47,6 @@ describe(' cold phase validation', () => { ({ runTimers } = testBed); }); - describe('timing', () => { - test(`doesn't allow empty timing`, async () => { - const { actions } = testBed; - - await actions.cold.setMinAgeValue(''); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test(`allows 0 for phase timing`, async () => { - const { actions } = testBed; - - await actions.cold.setMinAgeValue('0'); - - runTimers(); - - actions.expectErrorMessages([]); - }); - - test(`doesn't allow -1 for timing`, async () => { - const { actions } = testBed; - - await actions.cold.setMinAgeValue('-1'); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - }); - describe('replicas', () => { test(`doesn't allow -1 for replicas`, async () => { const { actions } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts deleted file mode 100644 index a13aaa02dcd06..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts +++ /dev/null @@ -1,81 +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 { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; - -describe(' delete phase validation', () => { - let testBed: EditPolicyTestBed; - let runTimers: () => void; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - server.restore(); - }); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: { data: ['node1'] }, - nodesByAttributes: { 'attribute:true': ['node1'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - httpRequestsMockHelpers.setNodesDetails('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - - await act(async () => { - testBed = await setup(); - }); - - const { component, actions } = testBed; - component.update(); - await actions.setPolicyName('mypolicy'); - await actions.delete.enablePhase(); - - ({ runTimers } = testBed); - }); - - describe('timing', () => { - test(`doesn't allow empty timing`, async () => { - const { actions } = testBed; - - await actions.delete.setMinAgeValue(''); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test(`allows 0 for phase timing`, async () => { - const { actions } = testBed; - - await actions.delete.setMinAgeValue('0'); - - runTimers(); - - actions.expectErrorMessages([]); - }); - - test(`doesn't allow -1 for timing`, async () => { - const { actions } = testBed; - - await actions.delete.setMinAgeValue('-1'); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts index 7c1d687b27e3d..ba260ee33e31e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -97,6 +97,17 @@ describe(' hot phase validation', () => { actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); + test(`doesn't allow decimals for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('5.5'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.integerRequired]); + }); + test(`doesn't allow -1 for max docs`, async () => { const { actions } = testBed; @@ -118,6 +129,17 @@ describe(' hot phase validation', () => { actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); + + test(`doesn't allow decimals for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('5.5'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.integerRequired]); + }); }); describe('forcemerge', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts new file mode 100644 index 0000000000000..52009902ab802 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; + +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupEnvironment } from '../../helpers/setup_environment'; + +describe(' timing validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] }); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + + ({ runTimers } = testBed); + }); + + [ + { + name: `doesn't allow empty timing`, + value: '', + error: [i18nTexts.editPolicy.errors.numberRequired], + }, + { + name: `allows 0 for timing`, + value: '0', + error: [], + }, + { + name: `doesn't allow -1 for timing`, + value: '-1', + error: [i18nTexts.editPolicy.errors.nonNegativeNumberRequired], + }, + { + name: `doesn't allow decimals for timing (with dot)`, + value: '5.5', + error: [i18nTexts.editPolicy.errors.integerRequired], + }, + { + name: `doesn't allow decimals for timing (with comma)`, + value: '5,5', + error: [i18nTexts.editPolicy.errors.integerRequired], + }, + ].forEach((testConfig: { name: string; value: string; error: string[] }) => { + ['warm', 'cold', 'delete', 'frozen'].forEach((phase: string) => { + const { name, value, error } = testConfig; + test(`${phase}: ${name}`, async () => { + const { actions } = testBed; + await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].enable(true); + await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue(value); + + runTimers(); + + actions.expectErrorMessages(error); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts index 2121dba8e06f6..bfb263e204de9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -47,38 +47,6 @@ describe(' warm phase validation', () => { ({ runTimers } = testBed); }); - describe('timing', () => { - test(`doesn't allow empty timing`, async () => { - const { actions } = testBed; - - await actions.warm.setMinAgeValue(''); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test(`allows 0 for phase timing`, async () => { - const { actions } = testBed; - - await actions.warm.setMinAgeValue('0'); - - runTimers(); - - actions.expectErrorMessages([]); - }); - - test(`doesn't allow -1 for timing`, async () => { - const { actions } = testBed; - - await actions.warm.setMinAgeValue('-1'); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - }); - describe('replicas', () => { test(`doesn't allow -1 for replicas`, async () => { const { actions } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index 61ceab1990c72..17dadb1c6b47e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -13,6 +13,7 @@ import { POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, } from '../constants'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { licensingMock } from '../../../../../licensing/public/mocks'; describe(' serialization', () => { let testBed: EditPolicyTestBed; @@ -58,7 +59,7 @@ describe(' serialization', () => { // Set max docs to test whether we keep the unknown fields in that object after serializing await actions.hot.setMaxDocs('1000'); // Remove the delete phase to ensure that we also correctly remove data - await actions.delete.disablePhase(); + await actions.delete.enable(false); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -91,6 +92,78 @@ describe(' serialization', () => { }, }); }); + + it('default policy (only policy name input) on enterprise license', async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('test_policy'); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + name: 'test_policy', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + }, + }); + }); + + it('default policy (only policy name input) on basic license', async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('test_policy'); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + name: 'test_policy', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + }, + }); + }); }); describe('hot phase', () => { @@ -408,7 +481,7 @@ describe(' serialization', () => { test('delete phase', async () => { const { actions } = testBed; - await actions.delete.enablePhase(); + await actions.delete.enable(true); await actions.setWaitForSnapshotPolicy('test'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index ffd4e2758ab86..8c90a738d2c09 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -60,7 +60,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; - const cloudDeploymentUrl = cloud?.cloudDeploymentUrl; + const cloudDeploymentUrl = cloud?.deploymentUrl; const renderNotice = () => { switch (allocationType) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index c0e489042586c..2b90d75fa6da0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -15,7 +15,7 @@ import { ifExistsNumberGreaterThanZero, ifExistsNumberNonNegative, rolloverThresholdsValidator, - minAgeValidator, + integerValidator, } from './validations'; const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); @@ -117,6 +117,20 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ serializer: serializers.stringToNumber, }); +const getMinAgeField = (defaultValue: string = '0') => ({ + defaultValue, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: ifExistsNumberNonNegative, + }, + { + validator: integerValidator, + }, + ], +}); export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ _meta: { hot: { @@ -254,6 +268,9 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ { validator: ifExistsNumberGreaterThanZero, }, + { + validator: integerValidator, + }, ], fieldsToValidateOnChange: rolloverFormPaths, }, @@ -268,6 +285,9 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ { validator: ifExistsNumberGreaterThanZero, }, + { + validator: integerValidator, + }, ], serializer: serializers.stringToNumber, fieldsToValidateOnChange: rolloverFormPaths, @@ -300,14 +320,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, warm: { - min_age: { - defaultValue: '0', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField(), actions: { allocate: { number_of_replicas: numberOfReplicasField, @@ -324,14 +337,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, cold: { - min_age: { - defaultValue: '0', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField(), actions: { allocate: { number_of_replicas: numberOfReplicasField, @@ -343,14 +349,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, frozen: { - min_age: { - defaultValue: '0', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField(), actions: { allocate: { number_of_replicas: numberOfReplicasField, @@ -362,14 +361,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, delete: { - min_age: { - defaultValue: '365', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField('365'), actions: { wait_for_snapshot: { policy: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 57112b0e1cb16..b10e3294f75c7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -254,7 +254,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( /** * FROZEN PHASE SERIALIZATION */ - if (_meta.frozen.enabled) { + if (_meta.frozen?.enabled) { draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {}; const frozenPhase = draft.phases.frozen!; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts index 5be49d25aed7f..ce85913d5db74 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts @@ -89,15 +89,11 @@ export const rolloverThresholdsValidator: ValidationFunc = ({ form, path }) => { } }; -export const minAgeValidator: ValidationFunc = (arg) => - numberGreaterThanField({ - than: 0, - allowEquality: true, - message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, - })({ - ...arg, - value: arg.value === '' ? -Infinity : parseInt(arg.value, 10), - }); +export const integerValidator: ValidationFunc = (arg) => { + if (!Number.isInteger(Number(arg.value ?? ''))) { + return { message: i18nTexts.editPolicy.errors.integerRequired }; + } +}; export const createPolicyNameValidations = ({ policies, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 47585fba38768..d9bd9b664d205 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -102,6 +102,12 @@ export const i18nTexts = { defaultMessage: 'Only numbers above 0 are allowed.', } ), + integerRequired: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.integerRequiredError', + { + defaultMessage: 'Only integers are allowed.', + } + ), maximumAgeRequiredMessage: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.maximumAgeMissingError', { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index f22e826a877ec..90cb48df4b8d9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -100,7 +100,7 @@ export const ComponentTable: FunctionComponent = ({ {...reactRouterNavigate(history, '/create_component_template')} > {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.createButtonLabel', { - defaultMessage: 'Create a component template', + defaultMessage: 'Create component template', })} , ], diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index a210831eef865..f6d739078002e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -24,6 +24,7 @@ import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor'; import { MetricsExplorerKueryBar } from '../../../../metrics_explorer/components/kuery_bar'; import { convertKueryToElasticSearchQuery } from '../../../../../../utils/kuery'; +import { useUiTracker } from '../../../../../../../../observability/public'; interface Props { jobType: 'hosts' | 'kubernetes'; @@ -40,6 +41,7 @@ export const JobSetupScreen = (props: Props) => { const k = useMetricK8sModuleContext(); const [filter, setFilter] = useState(''); const [filterQuery, setFilterQuery] = useState(''); + const trackMetric = useUiTracker({ app: 'infra_metrics' }); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', }); @@ -137,9 +139,25 @@ export const JobSetupScreen = (props: Props) => { useEffect(() => { if (setupStatus.type === 'succeeded') { + if (props.jobType === 'kubernetes') { + trackMetric({ metric: 'metrics_ml_anomaly_detection_k8s_enabled' }); + if ( + partitionField && + (partitionField.length !== 1 || partitionField[0] !== DEFAULT_K8S_PARTITION_FIELD) + ) { + trackMetric({ metric: 'metrics_ml_anomaly_detection_k8s_partition_changed' }); + } + } else { + trackMetric({ metric: 'metrics_ml_anomaly_detection_hosts_enabled' }); + if (partitionField) { + trackMetric({ metric: 'metrics_ml_anomaly_detection_hosts_partition_changed' }); + } + trackMetric({ metric: 'metrics_ml_anomaly_detection_hosts_enabled' }); + } + goHome(); } - }, [setupStatus, goHome]); + }, [setupStatus, props.jobType, partitionField, trackMetric, goHome]); return ( <> diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 542413118b330..1231a19f80ca2 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -166,23 +166,6 @@ export interface InfraFieldDef { [type: string]: InfraFieldDetails; } -export interface InfraTSVBResponse { - [key: string]: InfraTSVBPanel; -} - -export interface InfraTSVBPanel { - id: string; - series: InfraTSVBSeries[]; -} - -export interface InfraTSVBSeries { - id: string; - label: string; - data: InfraTSVBDataPoint[]; -} - -export type InfraTSVBDataPoint = [number, number]; - export type InfraRouteConfig = { method: RouteMethod; } & RouteConfig; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 451b2284ba310..0176361ede66f 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -14,7 +14,6 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { estypes } from '@elastic/elasticsearch'; import { InfraRouteConfig, - InfraTSVBResponse, InfraServerPluginSetupDeps, CallWithRequestParams, InfraDatabaseSearchResponse, @@ -34,6 +33,7 @@ import { RequestHandler } from '../../../../../../../src/core/server'; import { InfraConfig } from '../../../plugin'; import type { InfraPluginRequestHandlerContext } from '../../../types'; import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plugins/data/server'; +import { TimeseriesVisData } from '../../../../../../../src/plugins/vis_type_timeseries/server'; export class KibanaFramework { public router: IRouter; @@ -221,7 +221,7 @@ export class KibanaFramework { model: TSVBMetricModel, timerange: { min: number; max: number }, filters: any[] - ): Promise { + ): Promise { const { getVisData } = this.plugins.visTypeTimeseries; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 921634361f4a2..6009652e2d0b0 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -21,6 +21,7 @@ import { import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework'; import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { isVisSeriesData } from '../../../../../../../src/plugins/vis_type_timeseries/server'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { private framework: KibanaFramework; @@ -59,7 +60,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { return Promise.all(requests) .then((results) => { - return results.map((result) => { + return results.filter(isVisSeriesData).map((result) => { const metricIds = Object.keys(result).filter( (k) => !['type', 'uiRestrictions'].includes(k) ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss index 55630fa96d9b0..d51dee31e9f8e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss @@ -17,11 +17,6 @@ box-shadow: none; } - // Remove the box-shadow on all nested items - .pipelineProcessorsEditor__item { - box-shadow: none !important; - } - &__processorTypeLabel { line-height: $euiButtonHeightSmall; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 27035a8d269f2..320f99acd08f7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -158,7 +158,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( }; return ( - + = ({ {...reactRouterNavigate(history, '/create')} > {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { - defaultMessage: 'Create a pipeline', + defaultMessage: 'Create pipeline', })} , ], diff --git a/x-pack/plugins/lens/public/_variables.scss b/x-pack/plugins/lens/public/_variables.scss index 1c83a9a0499f1..824a0ce57186c 100644 --- a/x-pack/plugins/lens/public/_variables.scss +++ b/x-pack/plugins/lens/public/_variables.scss @@ -4,3 +4,8 @@ $lnsPanelMinWidth: $euiSize * 18; $lnsSuggestionHeight: 100px; $lnsSuggestionWidth: 150px; $lnsLayerPanelDimensionMargin: 8px; + +$lnsZLevel0: 0; +$lnsZLevel1: 1; +$lnsZLevel2: 2; +$lnsZLevel3: 3; \ No newline at end of file diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 9d5feec9f21e6..dbc10c751a649 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -531,7 +531,13 @@ export function App({ const { TopNavMenu } = navigation.ui; - const savingPermitted = Boolean(state.isSaveable && application.capabilities.visualize.save); + const savingToLibraryPermitted = Boolean( + state.isSaveable && application.capabilities.visualize.save + ); + const savingToDashboardPermitted = Boolean( + state.isSaveable && application.capabilities.dashboard?.showWriteControls + ); + const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { defaultMessage: 'unsaved', }); @@ -545,8 +551,10 @@ export function App({ state.isSaveable && state.activeData && Object.keys(state.activeData).length ), isByValueMode: getIsByValueMode(), + allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, showCancel: Boolean(state.isLinkedToOriginatingApp), - savingPermitted, + savingToLibraryPermitted, + savingToDashboardPermitted, actions: { exportToCSV: () => { if (!state.activeData) { @@ -577,7 +585,7 @@ export function App({ } }, saveAndReturn: () => { - if (savingPermitted && lastKnownDoc) { + if (savingToDashboardPermitted && lastKnownDoc) { // disabling the validation on app leave because the document has been saved. onAppLeave((actions) => { return actions.default(); @@ -597,7 +605,7 @@ export function App({ } }, showSaveModal: () => { - if (savingPermitted) { + if (savingToDashboardPermitted || savingToLibraryPermitted) { setState((s) => ({ ...s, isSaveModalVisible: true })); } }, @@ -697,6 +705,7 @@ export function App({ { const { originatingApp, + savingToLibraryPermitted, savedObjectsTagging, tagsIds, lastKnownDoc, @@ -85,13 +87,15 @@ export const SaveModal = (props: Props) => { { const saveToLibrary = Boolean(saveProps.addToLibrary); onSave(saveProps, { saveToLibrary }); }} onClose={onClose} documentInfo={{ - id: lastKnownDoc.savedObjectId, + // if the user cannot save to the library - treat this as a new document. + id: savingToLibraryPermitted ? lastKnownDoc.savedObjectId : undefined, title: lastKnownDoc.title || '', description: lastKnownDoc.description || '', }} diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 57ebe79af2219..9c5bc79ba044a 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -5,7 +5,7 @@ user-select: none; transition: $euiAnimSpeedFast ease-in-out; transition-property: background-color, border-color, opacity; - z-index: $euiZLevel1; + z-index: $lnsZLevel1; } .lnsDragDrop_ghost { @@ -18,7 +18,7 @@ left: 0; opacity: .9; transform: translate(-12px, 8px); - z-index: $euiZLevel3; + z-index: $lnsZLevel3; pointer-events: none; box-shadow: 0 0 0 $euiFocusRingSize $euiFocusRingColor; } @@ -58,7 +58,7 @@ // Drop area while hovering with item .lnsDragDrop-isActiveDropTarget { - z-index: $euiZLevel3; + z-index: $lnsZLevel3; @include lnsDroppableActiveHover; } @@ -90,7 +90,7 @@ height: 100%; &.lnsDragDrop__container-active { - z-index: $euiZLevel3; + z-index: $lnsZLevel3; } } @@ -111,7 +111,7 @@ } .lnsDragDrop-isActiveDropTarget { - z-index: $euiZLevel3; + z-index: $lnsZLevel3; } } @@ -119,7 +119,7 @@ transform: translateY(0); transition: transform $euiAnimSpeedFast ease-in-out; position: relative; - z-index: $euiZLevel1; + z-index: $lnsZLevel1; } .lnsDragDrop__keyboardHandler { @@ -151,7 +151,7 @@ opacity: 0; visibility: hidden; position: absolute; - z-index: $euiZLevel2; + z-index: $lnsZLevel2; right: calc(100% + #{$euiSizeS}); top: 0; transition: opacity $euiAnimSpeedFast ease-in-out; @@ -168,7 +168,7 @@ width: 30%; top: 0; left: -$euiSize; - z-index: $euiZLevel0; + z-index: $lnsZLevel0; } .lnsDragDrop__extraDropWrapper { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss index ffc0adf3e33ea..4631cc6392496 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss @@ -29,7 +29,7 @@ // This also means needing to add same amount of margin to page content and suggestion items padding: $euiSize $euiSize 0; position: relative; - z-index: $euiZLevel1; + z-index: $lnsZLevel1; &:first-child { padding-left: $euiSize; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index efe16c30935be..00eaadeaf8299 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -112,7 +112,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -151,7 +154,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -191,7 +194,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -231,7 +237,10 @@ describe('embeddable', () => { indexPatternService: ({ get: (id: string) => Promise.resolve({ id }), } as unknown) as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -266,7 +275,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -307,7 +319,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -338,7 +350,13 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(2); }); - it('should re-render when dashboard view/edit mode changes', async () => { + it('should re-render when dashboard view/edit mode changes if dynamic actions are set', async () => { + const sampleInput = ({ + id: '123', + enhancements: { + dynamicActions: {}, + }, + } as unknown) as LensEmbeddableInput; const embeddable = new Embeddable( { timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, @@ -346,7 +364,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -371,6 +392,13 @@ describe('embeddable', () => { viewMode: ViewMode.VIEW, }); + expect(expressionRenderer).toHaveBeenCalledTimes(1); + + embeddable.updateInput({ + ...sampleInput, + viewMode: ViewMode.VIEW, + }); + expect(expressionRenderer).toHaveBeenCalledTimes(2); }); @@ -382,7 +410,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -432,7 +463,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -482,7 +516,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -531,7 +568,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: ({ get: jest.fn() } as unknown) as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -569,7 +609,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -607,7 +650,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -645,7 +691,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -698,7 +747,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -767,7 +819,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -811,7 +866,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -855,7 +913,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index b395352b61477..a3316e0083d35 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -88,13 +88,13 @@ export interface LensEmbeddableDeps { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; - editable: boolean; indexPatternService: IndexPatternsContract; expressionRenderer: ReactExpressionRendererType; timefilter: TimefilterContract; basePath: IBasePath; getTrigger?: UiActionsStart['getTrigger'] | undefined; getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; + capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean }; } export class Embeddable @@ -129,7 +129,6 @@ export class Embeddable initialInput, { editApp: 'lens', - editable: deps.editable, }, parent ); @@ -172,7 +171,10 @@ export class Embeddable skip(1) ) .subscribe((input) => { - this.reload(); + // only reload if drilldowns are set + if (this.getInput().enhancements?.dynamicActions) { + this.reload(); + } }) ); @@ -323,7 +325,7 @@ export class Embeddable hasCompatibleActions={this.hasCompatibleActions} className={input.className} style={input.style} - canEdit={this.deps.editable && input.viewMode === 'edit'} + canEdit={this.getIsEditable() && input.viewMode === 'edit'} />, domNode ); @@ -448,6 +450,7 @@ export class Embeddable this.updateOutput({ ...this.getOutput(), defaultTitle: this.savedVis.title, + editable: this.getIsEditable(), title, editPath: getEditPath(savedObjectId), editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), @@ -455,6 +458,13 @@ export class Embeddable }); } + private getIsEditable() { + return ( + this.deps.capabilities.canSaveVisualizations || + (!this.inputIsRefType(this.getInput()) && this.deps.capabilities.canSaveDashboards) + ); + } + public inputIsRefType = ( input: LensByValueInput | LensByReferenceInput ): input is LensByReferenceInput => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index a676b7283671c..1a4962bd1fe8e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -53,7 +53,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { public isEditable = async () => { const { capabilities } = await this.getStartServices(); - return capabilities.visualize.save as boolean; + return Boolean(capabilities.visualize.save || capabilities.dashboard?.showWriteControls); }; canCreateNew() { @@ -86,6 +86,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { coreHttp, attributeService, indexPatternService, + capabilities, } = await this.getStartServices(); const { Embeddable } = await import('../../async_services'); @@ -96,11 +97,14 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, timefilter, expressionRenderer, - editable: await this.isEditable(), basePath: coreHttp.basePath, getTrigger: uiActions?.getTrigger, getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions, documentToExpression, + capabilities: { + canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), + canSaveVisualizations: Boolean(capabilities.visualize.save), + }, }, input, parent diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index fa5a9f9289e92..9b53e59f96792 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -19,7 +19,9 @@ export type { SeriesType, ValueLabelConfig, YAxisMode, + XYCurveType, } from './xy_visualization/types'; +export type { DataType } from './types'; export type { PieVisualizationState, PieLayerState, 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 14834adfc33cc..0ea533e22e4d9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -474,53 +474,6 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); - it('should add the suffix to the remap column id if provided by the operation', async () => { - const queryBaseState: IndexPatternBaseState = { - currentIndexPatternId: '1', - layers: { - first: { - indexPatternId: '1', - columnOrder: ['def', 'abc'], - columns: { - abc: { - label: '23rd percentile', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'percentile', - params: { - percentile: 23, - }, - }, - def: { - label: 'Terms', - dataType: 'string', - isBucketed: true, - operationType: 'terms', - sourceField: 'source', - params: { - size: 5, - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'asc', - }, - }, - }, - }, - }, - }; - - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; - expect(Object.keys(JSON.parse(ast.chain[1].arguments.idMap[0] as string))).toEqual([ - 'col-0-def', - // col-1 is the auto naming of esasggs, abc is the specified column id, .23 is the generated suffix - 'col-1-abc.23', - ]); - }); - it('should wrap filtered metrics in filtered metric aggregation', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 2d2227396afa6..b7e92a0b54952 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -64,13 +64,6 @@ export function getInvalidFieldMessage( : undefined; } -export function getEsAggsSuffix(column: IndexPatternColumn) { - const operationDefinition = operationDefinitionMap[column.operationType]; - return operationDefinition.input === 'field' && operationDefinition.getEsAggsSuffix - ? operationDefinition.getEsAggsSuffix(column) - : ''; -} - export function getSafeName(name: string, indexPattern: IndexPattern): string { const field = indexPattern.getFieldByName(name); return field diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index b3aa93b062eb1..0b63dc6ece974 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -311,13 +311,6 @@ interface FieldBasedOperationDefinition { layer: IndexPatternLayer, uiSettings: IUiSettingsClient ) => ExpressionAstFunction; - /** - * Optional function to return the suffix used for ES bucket paths and esaggs column id. - * This is relevant for multi metrics to pick the right value. - * - * @param column The current column - */ - getEsAggsSuffix?: (column: C) => string; /** * Validate that the operation has the right preconditions in the state. For example: * diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 9ac91be5a17ec..c14ff9f86f602 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -127,7 +127,7 @@ describe('percentile', () => { expect(esAggsFn).toEqual( expect.objectContaining({ arguments: expect.objectContaining({ - percents: [23], + percentile: [23], field: ['a'], }), }) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index 639b9e3a95c47..dd0f3b978da5f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -51,6 +51,7 @@ export const percentileOperation: OperationDefinition { if (supportedFieldTypes.includes(fieldType) && aggregatable && !aggregationRestrictions) { return { @@ -86,6 +87,7 @@ export const percentileOperation: OperationDefinition { - return buildExpressionFunction('aggPercentiles', { - id: columnId, - enabled: true, - schema: 'metric', - field: column.sourceField, - percents: [column.params.percentile], - }).toAst(); - }, - getEsAggsSuffix: (column) => { - const value = column.params.percentile; - return `.${value}`; + return buildExpressionFunction( + 'aggSinglePercentile', + { + id: columnId, + enabled: true, + schema: 'metric', + field: column.sourceField, + percentile: column.params.percentile, + } + ).toAst(); }, getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index a4a061db04797..857e8b3605cfc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -23,7 +23,7 @@ import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesInput } from './values_input'; -import { getEsAggsSuffix, getInvalidFieldMessage } from '../helpers'; +import { getInvalidFieldMessage } from '../helpers'; import type { IndexPatternLayer } from '../../../types'; function ofName(name?: string) { @@ -137,11 +137,7 @@ export const termsOperation: OperationDefinition { }) ); }); - - it('should include esaggs suffix from other columns in orderby argument', () => { - const termsColumn = layer.columns.col1 as TermsIndexPatternColumn; - const esAggsFn = termsOperation.toEsAggsFn( - { - ...termsColumn, - params: { - ...termsColumn.params, - otherBucket: true, - orderBy: { type: 'column', columnId: 'abcde' }, - }, - }, - 'col1', - {} as IndexPattern, - { - ...layer, - columns: { - ...layer.columns, - abcde: { - dataType: 'number', - isBucketed: false, - operationType: 'percentile', - sourceField: 'abc', - label: '', - params: { - percentile: 12, - }, - }, - }, - }, - uiSettingsMock - ); - expect(esAggsFn).toEqual( - expect.objectContaining({ - arguments: expect.objectContaining({ - orderBy: ['abcde.12'], - }), - }) - ); - }); }); describe('onFieldChange', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts index 2e4de8b52dacb..34579927cfe19 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { Datatable } from 'src/plugins/expressions/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginStart, TimeRange } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; import { getTimeScaleFunction, TimeScaleArgs } from './time_scale'; @@ -44,9 +44,25 @@ describe('time_scale', () => { targetUnit: 'h', }; + function setDateHistogramMeta(options: { + timeZone: string; + timeRange: TimeRange; + interval: string; + }) { + emptyTable.columns[0].meta.source = 'esaggs'; + emptyTable.columns[0].meta.sourceParams = { + type: 'date_histogram', + params: { + used_interval: options.interval, + used_time_zone: options.timeZone, + }, + appliedTimeRange: options.timeRange, + }; + } + beforeEach(() => { dataMock = dataPluginMock.createStartContract(); - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T00:00:00.000Z', @@ -156,7 +172,7 @@ describe('time_scale', () => { }); it('should be able to scale up as well', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T12:00:00.000Z', @@ -196,7 +212,7 @@ describe('time_scale', () => { }); it('can scale starting from unit multiple target intervals', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T13:00:00.000Z', @@ -238,7 +254,7 @@ describe('time_scale', () => { }); it('take start and end of timerange into account', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T12:00:00.000Z', @@ -283,7 +299,7 @@ describe('time_scale', () => { }); it('should respect DST switches', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'Europe/Berlin', timeRange: { from: '2020-10-23T00:00:00.000+02:00', @@ -323,7 +339,7 @@ describe('time_scale', () => { }); it('take leap years into account', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2010-01-01T00:00:00.000Z', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 33395de389125..368e06110efc9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -97,8 +97,8 @@ export function getTimeScaleFunction(data: DataPublicPluginStart) { } const targetUnitInMs = unitInMs[targetUnit]; - const timeInfo = await data.search.aggs.getDateMetaByDatatableColumn(dateColumnDefinition); - const intervalDuration = timeInfo && search.aggs.parseInterval(timeInfo.interval); + const timeInfo = search.aggs.getDateHistogramMetaDataByDatatableColumn(dateColumnDefinition); + const intervalDuration = timeInfo?.interval && search.aggs.parseInterval(timeInfo.interval); if (!timeInfo || !intervalDuration) { throw new Error( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index d786d781199b6..b272e5476aa63 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -23,7 +23,6 @@ import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; import { OriginalColumn } from './rename_columns'; import { dateHistogramOperation } from './operations/definitions'; -import { getEsAggsSuffix } from './operations/definitions/helpers'; function getExpressionForLayer( layer: IndexPatternLayer, @@ -104,10 +103,9 @@ function getExpressionForLayer( const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { const esAggsId = `col-${columnEntries.length === 1 ? 0 : index}-${colId}`; - const suffix = getEsAggsSuffix(column); return { ...currentIdMap, - [`${esAggsId}${suffix}`]: { + [esAggsId]: { ...column, id: colId, }, diff --git a/x-pack/plugins/lens/public/mocks.ts b/x-pack/plugins/lens/public/mocks.tsx similarity index 68% rename from x-pack/plugins/lens/public/mocks.ts rename to x-pack/plugins/lens/public/mocks.tsx index fd1e38db242a8..743846d81213c 100644 --- a/x-pack/plugins/lens/public/mocks.ts +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -5,16 +5,20 @@ * 2.0. */ +import React from 'react'; import { LensPublicStart } from '.'; +import { visualizationTypes } from './xy_visualization/types'; export type Start = jest.Mocked; const createStartContract = (): Start => { const startContract: Start = { - EmbeddableComponent: jest.fn(() => null), + EmbeddableComponent: jest.fn(() => { + return Lens Embeddable Component; + }), canUseEditor: jest.fn(() => true), navigateToPrefilledEditor: jest.fn(), - getXyVisTypes: jest.fn(), + getXyVisTypes: jest.fn().mockReturnValue(new Promise(() => visualizationTypes)), }; return startContract; }; 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 4d6eac6a87e48..b6bb2908b9ed2 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -251,6 +251,7 @@ export function PieComponent( onClickValue(desanitizeFilterContext(context)); }; + return ( { - const { - indexPattern, - geoFieldName, - filterByMapBounds, - scalingType, - topHitsSplitField, - topHitsSize, - } = this.state; + const { indexPattern, geoFieldName, filterByMapBounds, scalingType } = this.state; const sourceConfig = indexPattern && geoFieldName @@ -113,8 +103,6 @@ export class CreateSourceEditor extends Component { geoField: geoFieldName, filterByMapBounds, scalingType, - topHitsSplitField, - topHitsSize, } : null; this.props.onSourceConfigChange(sourceConfig); @@ -167,9 +155,6 @@ export class CreateSourceEditor extends Component { ) : null } - termFields={getTermsFields(this.state.indexPattern.fields)} - topHitsSplitField={this.state.topHitsSplitField} - topHitsSize={this.state.topHitsSize} /> ); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index c0606b5f4aec6..26771c1bed023 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -10,7 +10,6 @@ import React from 'react'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; -// @ts-ignore import { ESSearchSource, sourceTitle } from './es_search_source'; import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; import { VectorLayer } from '../../layers/vector_layer'; 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 eae00710c4c25..168448b6f72a0 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 @@ -60,6 +60,7 @@ import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { DataRequest } from '../../util/data_request'; import { SortDirection, SortDirectionNumeric } from '../../../../../../../src/plugins/data/common'; import { isValidStringConfig } from '../../util/valid_string_config'; +import { TopHitsUpdateSourceEditor } from './top_hits'; export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', @@ -166,6 +167,22 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye } renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null { + if (this._isTopHits()) { + return ( + + ); + } + const getGeoField = () => { return this._getGeoField(); }; @@ -180,8 +197,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye sortOrder={this._descriptor.sortOrder} scalingType={this._descriptor.scalingType} filterByMapBounds={this.isFilterByMapBounds()} - topHitsSplitField={this._descriptor.topHitsSplitField} - topHitsSize={this._descriptor.topHitsSize} /> ); } @@ -658,6 +673,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye getSyncMeta(): VectorSourceSyncMeta | null { return { + filterByMapBounds: this._descriptor.filterByMapBounds, sortField: this._descriptor.sortField, sortOrder: this._descriptor.sortOrder, scalingType: this._descriptor.scalingType, diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts index 73e7963024471..75217c0a29c08 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts @@ -11,3 +11,4 @@ export { createDefaultLayerDescriptor, esDocumentsLayerWizardConfig, } from './es_documents_layer_wizard'; +export { esTopHitsLayerWizardConfig } from './top_hits'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx index fe47208c32690..b02eacc133467 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx @@ -26,8 +26,6 @@ const defaultProps = { scalingType: SCALING_TYPES.LIMIT, supportsClustering: true, termFields: [], - topHitsSplitField: null, - topHitsSize: 1, }; describe('scaling form', () => { @@ -48,12 +46,4 @@ describe('scaling form', () => { expect(component).toMatchSnapshot(); }); - - test('should render top hits form when scaling type is TOP_HITS', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx index 6190c7ed8df3f..b9ce43dbbdad4 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx @@ -19,19 +19,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SingleFieldSelect } from '../../../components/single_field_select'; import { getIndexPatternService } from '../../../kibana_services'; -// @ts-ignore -import { ValidatedRange } from '../../../components/validated_range'; -import { - DEFAULT_MAX_INNER_RESULT_WINDOW, - DEFAULT_MAX_RESULT_WINDOW, - LAYER_TYPE, - SCALING_TYPES, -} from '../../../../common/constants'; -// @ts-ignore +import { DEFAULT_MAX_RESULT_WINDOW, LAYER_TYPE, SCALING_TYPES } from '../../../../common/constants'; import { loadIndexSettings } from './load_index_settings'; -import { IFieldType } from '../../../../../../../src/plugins/data/public'; import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; interface Props { @@ -41,19 +31,14 @@ interface Props { scalingType: SCALING_TYPES; supportsClustering: boolean; clusteringDisabledReason?: string | null; - termFields: IFieldType[]; - topHitsSplitField: string | null; - topHitsSize: number; } interface State { - maxInnerResultWindow: number; maxResultWindow: number; } export class ScalingForm extends Component { state = { - maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW, maxResultWindow: DEFAULT_MAX_RESULT_WINDOW, }; _isMounted = false; @@ -70,11 +55,9 @@ export class ScalingForm extends Component { async loadIndexSettings() { try { const indexPattern = await getIndexPatternService().get(this.props.indexPatternId); - const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings( - indexPattern!.title - ); + const { maxResultWindow } = await loadIndexSettings(indexPattern!.title); if (this._isMounted) { - this.setState({ maxInnerResultWindow, maxResultWindow }); + this.setState({ maxResultWindow }); } } catch (err) { return; @@ -98,71 +81,6 @@ export class ScalingForm extends Component { this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked }); }; - _onTopHitsSplitFieldChange = (topHitsSplitField?: string) => { - if (!topHitsSplitField) { - return; - } - this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField }); - }; - - _onTopHitsSizeChange = (size: number) => { - this.props.onChange({ propName: 'topHitsSize', value: size }); - }; - - _renderTopHitsForm() { - let sizeSlider; - if (this.props.topHitsSplitField) { - sizeSlider = ( - - - - ); - } - - return ( - - - - - - {sizeSlider} - - ); - } - _renderClusteringRadio() { const clusteringRadio = ( { render() { let filterByBoundsSwitch; - if ( - this.props.scalingType === SCALING_TYPES.TOP_HITS || - this.props.scalingType === SCALING_TYPES.LIMIT - ) { + if (this.props.scalingType === SCALING_TYPES.LIMIT) { filterByBoundsSwitch = ( { ); } - let topHitsOptionsForm = null; - if (this.props.scalingType === SCALING_TYPES.TOP_HITS) { - topHitsOptionsForm = ( - - - {this._renderTopHitsForm()} - - ); - } - return ( @@ -267,21 +172,12 @@ export class ScalingForm extends Component { checked={this.props.scalingType === SCALING_TYPES.LIMIT} onChange={() => this._onScalingTypeChange(SCALING_TYPES.LIMIT)} /> - this._onScalingTypeChange(SCALING_TYPES.TOP_HITS)} - /> {this._renderClusteringRadio()} {this._renderMVTRadio()}
{filterByBoundsSwitch} - {topHitsOptionsForm} ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx new file mode 100644 index 0000000000000..ec656be3efeae --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { SCALING_TYPES } from '../../../../../common/constants'; +import { GeoFieldSelect } from '../../../../components/geo_field_select'; +import { GeoIndexPatternSelect } from '../../../../components/geo_index_pattern_select'; +import { getGeoFields, getTermsFields, getSortFields } from '../../../../index_pattern_util'; +import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types'; +import { + IndexPattern, + IFieldType, + SortDirection, +} from '../../../../../../../../src/plugins/data/common'; +import { TopHitsForm } from './top_hits_form'; +import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view'; + +interface Props { + onSourceConfigChange: (sourceConfig: Partial | null) => void; +} + +interface State { + indexPattern: IndexPattern | null; + geoFields: IFieldType[]; + geoFieldName: string | null; + sortField: string | null; + sortFields: IFieldType[]; + sortOrder: SortDirection; + termFields: IFieldType[]; + topHitsSplitField: string | null; + topHitsSize: number; +} + +export class CreateSourceEditor extends Component { + state: State = { + indexPattern: null, + geoFields: [], + geoFieldName: null, + sortField: null, + sortFields: [], + sortOrder: SortDirection.desc, + termFields: [], + topHitsSplitField: null, + topHitsSize: 1, + }; + + _onIndexPatternSelect = (indexPattern: IndexPattern) => { + const geoFields = getGeoFields(indexPattern.fields); + + this.setState( + { + indexPattern, + geoFields, + geoFieldName: geoFields.length ? geoFields[0].name : null, + sortField: indexPattern.timeFieldName ? indexPattern.timeFieldName : null, + sortFields: getSortFields(indexPattern.fields), + termFields: getTermsFields(indexPattern.fields), + topHitsSplitField: null, + }, + this._previewLayer + ); + }; + + _onGeoFieldSelect = (geoFieldName?: string) => { + this.setState({ geoFieldName: geoFieldName ? geoFieldName : null }, this._previewLayer); + }; + + _onTopHitsPropChange = ({ propName, value }: OnSourceChangeArgs) => { + this.setState( + // @ts-expect-error + { [propName]: value }, + this._previewLayer + ); + }; + + _previewLayer = () => { + const { + indexPattern, + geoFieldName, + sortField, + sortOrder, + topHitsSplitField, + topHitsSize, + } = this.state; + + const tooltipProperties: string[] = []; + if (topHitsSplitField) { + tooltipProperties.push(topHitsSplitField); + } + if (indexPattern && indexPattern.timeFieldName) { + tooltipProperties.push(indexPattern.timeFieldName); + } + + const sourceConfig = + indexPattern && geoFieldName && sortField && topHitsSplitField + ? { + indexPatternId: indexPattern.id, + geoField: geoFieldName, + scalingType: SCALING_TYPES.TOP_HITS, + sortField, + sortOrder, + tooltipProperties, + topHitsSplitField, + topHitsSize, + } + : null; + this.props.onSourceConfigChange(sourceConfig); + }; + + _renderGeoSelect() { + return this.state.indexPattern ? ( + + ) : null; + } + + _renderTopHitsPanel() { + if (!this.state.indexPattern || !this.state.indexPattern.id || !this.state.geoFieldName) { + return null; + } + + return ( + + ); + } + + render() { + return ( + + + + {this._renderGeoSelect()} + + {this._renderTopHitsPanel()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/index.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/index.ts new file mode 100644 index 0000000000000..135ed7c991b3a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/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 { TopHitsUpdateSourceEditor } from './update_source_editor'; +export { esTopHitsLayerWizardConfig } from './wizard'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx new file mode 100644 index 0000000000000..e4f196e5e8a85 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx @@ -0,0 +1,190 @@ +/* + * Copyright 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, { ChangeEvent, Component, Fragment } from 'react'; +import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SingleFieldSelect } from '../../../../components/single_field_select'; +import { getIndexPatternService } from '../../../../kibana_services'; +// @ts-expect-error +import { ValidatedRange } from '../../../../components/validated_range'; +import { DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../../../../common/constants'; +import { loadIndexSettings } from '../load_index_settings'; +import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view'; +import { IFieldType, SortDirection } from '../../../../../../../../src/plugins/data/public'; + +interface Props { + indexPatternId: string; + isColumnCompressed?: boolean; + onChange: (args: OnSourceChangeArgs) => void; + sortField: string; + sortFields: IFieldType[]; + sortOrder: SortDirection; + termFields: IFieldType[]; + topHitsSplitField: string | null; + topHitsSize: number; +} + +interface State { + maxInnerResultWindow: number; +} + +export class TopHitsForm extends Component { + state = { + maxInnerResultWindow: DEFAULT_MAX_INNER_RESULT_WINDOW, + }; + _isMounted = false; + + componentDidMount() { + this._isMounted = true; + this.loadIndexSettings(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + _onTopHitsSplitFieldChange = (topHitsSplitField?: string) => { + if (!topHitsSplitField) { + return; + } + this.props.onChange({ propName: 'topHitsSplitField', value: topHitsSplitField }); + }; + + _onTopHitsSizeChange = (size: number) => { + this.props.onChange({ propName: 'topHitsSize', value: size }); + }; + + _onSortFieldChange = (sortField?: string) => { + this.props.onChange({ propName: 'sortField', value: sortField }); + }; + + _onSortOrderChange = (event: ChangeEvent) => { + this.props.onChange({ propName: 'sortOrder', value: event.target.value }); + }; + + async loadIndexSettings() { + try { + const indexPattern = await getIndexPatternService().get(this.props.indexPatternId); + const { maxInnerResultWindow } = await loadIndexSettings(indexPattern!.title); + if (this._isMounted) { + this.setState({ maxInnerResultWindow }); + } + } catch (err) { + return; + } + } + + render() { + let sizeSlider; + let sortField; + let sortOrder; + if (this.props.topHitsSplitField) { + sizeSlider = ( + + + + ); + + sortField = ( + + + + ); + + sortOrder = ( + + + + ); + } + + return ( + + + + + + {sizeSlider} + + {sortField} + + {sortOrder} + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx new file mode 100644 index 0000000000000..90553d47e644a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component, Fragment } from 'react'; +import { EuiFormRow, EuiTitle, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FIELD_ORIGIN } from '../../../../../common/constants'; +import { TooltipSelector } from '../../../../components/tooltip_selector'; + +import { getIndexPatternService } from '../../../../kibana_services'; +import { getTermsFields, getSortFields, getSourceFields } from '../../../../index_pattern_util'; +import { SortDirection, IFieldType } from '../../../../../../../../src/plugins/data/public'; +import { ESDocField } from '../../../fields/es_doc_field'; +import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view'; +import { TopHitsForm } from './top_hits_form'; +import { ESSearchSource } from '../es_search_source'; +import { IField } from '../../../fields/field'; + +interface Props { + filterByMapBounds: boolean; + indexPatternId: string; + onChange: (args: OnSourceChangeArgs) => void; + tooltipFields: IField[]; + topHitsSplitField: string; + topHitsSize: number; + sortField: string; + sortOrder: SortDirection; + source: ESSearchSource; +} + +interface State { + loadError?: string; + sourceFields: IField[]; + termFields: IFieldType[]; + sortFields: IFieldType[]; +} + +export class TopHitsUpdateSourceEditor extends Component { + private _isMounted = false; + + state: State = { + sourceFields: [], + termFields: [], + sortFields: [], + }; + + componentDidMount() { + this._isMounted = true; + this.loadFields(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async loadFields() { + let indexPattern; + try { + indexPattern = await getIndexPatternService().get(this.props.indexPatternId); + } catch (err) { + if (this._isMounted) { + this.setState({ + loadError: i18n.translate('xpack.maps.source.esSearch.loadErrorMessage', { + defaultMessage: `Unable to find Index pattern {id}`, + values: { + id: this.props.indexPatternId, + }, + }), + }); + } + return; + } + + if (!this._isMounted) { + return; + } + + const rawTooltipFields = getSourceFields(indexPattern.fields); + const sourceFields = rawTooltipFields.map((field) => { + return new ESDocField({ + fieldName: field.name, + source: this.props.source, + origin: FIELD_ORIGIN.SOURCE, + }); + }); + + this.setState({ + sourceFields, + termFields: getTermsFields(indexPattern.fields), + sortFields: getSortFields(indexPattern.fields), + }); + } + _onTooltipPropertiesChange = (propertyNames: string[]) => { + this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); + }; + + _onFilterByMapBoundsChange = (event: EuiSwitchEvent) => { + this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked }); + }; + + render() { + return ( + + + +
+ +
+
+ + + + +
+ + + + +
+ +
+
+ + + + + + + + +
+ +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx new file mode 100644 index 0000000000000..e02ada305ecff --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/wizard.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CreateSourceEditor } from './create_source_editor'; +import { LayerWizard, RenderWizardArguments } from '../../../layers/layer_wizard_registry'; +import { VectorLayer } from '../../../layers/vector_layer'; +import { LAYER_WIZARD_CATEGORY } from '../../../../../common/constants'; +import { TopHitsLayerIcon } from '../../../layers/icons/top_hits_layer_icon'; +import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types'; +import { ESSearchSource } from '../es_search_source'; + +export const esTopHitsLayerWizardConfig: LayerWizard = { + categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], + description: i18n.translate('xpack.maps.source.topHitsDescription', { + defaultMessage: + 'Display the most relevant documents per entity, e.g. the most recent GPS hits per vehicle.', + }), + icon: TopHitsLayerIcon, + renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: Partial | null) => { + if (!sourceConfig) { + previewLayers([]); + return; + } + + const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayers([layerDescriptor]); + }; + return ; + }, + title: i18n.translate('xpack.maps.source.topHitsTitle', { + defaultMessage: 'Top hits per entity', + }), +}; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js index 1e870f423171f..8632666011065 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js @@ -8,6 +8,7 @@ import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiSelect, EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FIELD_ORIGIN } from '../../../../common/constants'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { TooltipSelector } from '../../../components/tooltip_selector'; @@ -15,7 +16,6 @@ import { getIndexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { getGeoTileAggNotSupportedReason, - getTermsFields, getSourceFields, supportsGeoTileAgg, } from '../../../index_pattern_util'; @@ -33,14 +33,11 @@ export class UpdateSourceEditor extends Component { sortField: PropTypes.string, sortOrder: PropTypes.string.isRequired, scalingType: PropTypes.string.isRequired, - topHitsSplitField: PropTypes.string, - topHitsSize: PropTypes.number.isRequired, source: PropTypes.object, }; state = { sourceFields: null, - termFields: null, sortFields: null, supportsClustering: false, mvtDisabledReason: null, @@ -94,6 +91,7 @@ export class UpdateSourceEditor extends Component { return new ESDocField({ fieldName: field.name, source: this.props.source, + origin: FIELD_ORIGIN.SOURCE, }); }); @@ -102,7 +100,6 @@ export class UpdateSourceEditor extends Component { clusteringDisabledReason: getGeoTileAggNotSupportedReason(geoField), mvtDisabledReason: null, sourceFields: sourceFields, - termFields: getTermsFields(indexPattern.fields), //todo change term fields to use fields sortFields: indexPattern.fields.filter( (field) => field.sortable && !indexPatterns.isNestedField(field) ), //todo change sort fields to use fields @@ -212,9 +209,6 @@ export class UpdateSourceEditor extends Component { scalingType={this.props.scalingType} supportsClustering={this.state.supportsClustering} clusteringDisabledReason={this.state.clusteringDisabledReason} - termFields={this.state.termFields} - topHitsSplitField={this.props.topHitsSplitField} - topHitsSize={this.props.topHitsSize} />
); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.test.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.test.js index 00a7b2b0b3490..f54947bc91d19 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.test.js @@ -26,8 +26,6 @@ const defaultProps = { tooltipFields: [], sortOrder: 'DESC', scalingType: SCALING_TYPES.LIMIT, - topHitsSplitField: 'trackId', - topHitsSize: 1, }; test('should render update source editor', async () => { diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/create_source_editor.js index 436d05fdbc6e8..1278d84f103da 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/create_source_editor.js @@ -8,7 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiSelect, EuiFormRow, EuiPanel } from '@elastic/eui'; -import { getKibanaRegionList } from '../../../meta'; +import { getKibanaRegionList } from '../../../util'; import { i18n } from '@kbn/i18n'; export function CreateSourceEditor({ onSourceConfigChange }) { diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 907b80e6405a6..9091e03fdf7f5 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -13,7 +13,7 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; import { VectorLayer } from '../../layers/vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; -import { getKibanaRegionList } from '../../../meta'; +import { getKibanaRegionList } from '../../../util'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const kibanaRegionMapLayerWizardConfig: LayerWizard = { diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts index 0f778f194ce3f..12e4b00c3c7b9 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source'; -import { getKibanaRegionList } from '../../../meta'; +import { fetchGeoJson, getKibanaRegionList } from '../../../util'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { FIELD_ORIGIN, FORMAT_TYPE, SOURCE_TYPES } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; @@ -79,11 +79,12 @@ export class KibanaRegionmapSource extends AbstractVectorSource { async getGeoJsonWithMeta(): Promise { const vectorFileMeta = await this.getVectorFileMeta(); - const featureCollection = await AbstractVectorSource.getGeoJson({ - format: vectorFileMeta.format.type as FORMAT_TYPE, - featureCollectionPath: vectorFileMeta.meta.feature_collection_path, - fetchUrl: vectorFileMeta.url, - }); + const featureCollection = await fetchGeoJson( + vectorFileMeta.url, + vectorFileMeta.format.type as FORMAT_TYPE, + vectorFileMeta.meta.feature_collection_path + ); + return { data: featureCollection, meta: {}, diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js index 8ec57d2b6f4fb..4d6939a3b7d45 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui'; -import { getKibanaTileMap } from '../../../meta'; +import { getKibanaTileMap } from '../../../util'; import { i18n } from '@kbn/i18n'; export function CreateSourceEditor({ onSourceConfigChange }) { diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index 8d18cda4e70dd..26893086ba8f7 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -13,7 +13,7 @@ import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; import { TileLayer } from '../../layers/tile_layer/tile_layer'; -import { getKibanaTileMap } from '../../../meta'; +import { getKibanaTileMap } from '../../../util'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_tilemap_source.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_tilemap_source.js index 0b88fe2e13905..94d082d8744e8 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_tilemap_source.js +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_tilemap_source.js @@ -6,7 +6,7 @@ */ import { AbstractTMSSource } from '../tms_source'; -import { getKibanaTileMap } from '../../../meta'; +import { getKibanaTileMap } from '../../../util'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import _ from 'lodash'; diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 7c2aaf714c34e..25e3595d6dffa 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -84,7 +84,7 @@ export class AbstractSource implements ISource { } async supportsFitToBounds(): Promise { - return true; + return false; } /** diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 5474e62e175d1..e86e459851c70 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -5,13 +5,9 @@ * 2.0. */ -// @ts-expect-error -import * as topojson from 'topojson-client'; -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; import { FeatureCollection, GeoJsonProperties } from 'geojson'; import { Filter, TimeRange } from 'src/plugins/data/public'; -import { FORMAT_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { TooltipProperty, ITooltipProperty } from '../../tooltips/tooltip_property'; import { AbstractSource, ISource } from '../source'; import { IField } from '../../fields/field'; @@ -85,48 +81,6 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource { } export class AbstractVectorSource extends AbstractSource implements IVectorSource { - static async getGeoJson({ - format, - featureCollectionPath, - fetchUrl, - }: { - format: FORMAT_TYPE; - featureCollectionPath: string; - fetchUrl: string; - }) { - let fetchedJson; - try { - const response = await fetch(fetchUrl); - if (!response.ok) { - throw new Error('Request failed'); - } - fetchedJson = await response.json(); - } catch (e) { - throw new Error( - i18n.translate('xpack.maps.source.vetorSource.requestFailedErrorMessage', { - defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`, - values: { fetchUrl }, - }) - ); - } - - if (format === FORMAT_TYPE.GEOJSON) { - return fetchedJson; - } - - if (format === FORMAT_TYPE.TOPOJSON) { - const features = _.get(fetchedJson, `objects.${featureCollectionPath}`); - return topojson.feature(fetchedJson, features); - } - - throw new Error( - i18n.translate('xpack.maps.source.vetorSource.formatErrorMessage', { - defaultMessage: `Unable to fetch vector shapes from url: {format}`, - values: { format }, - }) - ); - } - getFieldNames(): string[] { return []; } @@ -147,6 +101,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return false; } + async supportsFitToBounds(): Promise { + return true; + } + async getBoundsForFilters( boundsFilters: BoundsFilters, registerCancelCallback: (callback: () => void) => void diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx index 5c2e11813bb5f..47c2012d6ed8f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx @@ -25,9 +25,9 @@ export class DataMappingPopover extends Component { }; _togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); }; _closePopover = () => { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx new file mode 100644 index 0000000000000..4550a27ac2d9a --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 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('../components/vector_style_editor', () => ({ + VectorStyleEditor: () => { + return
mockVectorStyleEditor
; + }, +})); + +import React from 'react'; + +// @ts-ignore +import { DynamicTextProperty } from './dynamic_text_property'; +import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; +import { IField } from '../../../fields/field'; +import { Map as MbMap } from 'mapbox-gl'; +import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; +import { IVectorLayer } from '../../../layers/vector_layer'; + +export class MockMbMap { + _paintPropertyCalls: unknown[]; + _lastTextFieldValue: unknown | undefined; + + constructor(lastTextFieldValue?: unknown) { + this._paintPropertyCalls = []; + this._lastTextFieldValue = lastTextFieldValue; + } + setLayoutProperty(layerId: string, propName: string, value: undefined | 'string') { + if (propName !== 'text-field') { + throw new Error('should only use to test `text-field`'); + } + this._lastTextFieldValue = value; + this._paintPropertyCalls.push([layerId, value]); + } + + getLayoutProperty(layername: string, propName: string): unknown | undefined { + if (propName !== 'text-field') { + throw new Error('should only use to test `text-field`'); + } + return this._lastTextFieldValue; + } + + getPaintPropertyCalls(): unknown[] { + return this._paintPropertyCalls; + } +} + +const makeProperty = (mockStyle: MockStyle, field: IField | null) => { + return new DynamicTextProperty( + {}, + VECTOR_STYLES.LABEL_TEXT, + field, + (new MockLayer(mockStyle) as unknown) as IVectorLayer, + () => { + return (value: RawValue) => value + '_format'; + } + ); +}; + +describe('syncTextFieldWithMb', () => { + describe('with field', () => { + test('Should set', async () => { + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), mockField); + const mockMbMap = (new MockMbMap() as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([ + ['foobar', ['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], '']], + ]); + }); + }); + + describe('without field', () => { + test('Should clear', async () => { + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null); + const mockMbMap = (new MockMbMap([ + 'foobar', + ['coalesce', ['get', '__kbn__dynamic__foobar__labelText'], ''], + ]) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', undefined]]); + }); + + test('Should not clear when already cleared', async () => { + // This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated. + // This triggers a refetch of the tile during panning and zooming + // This affects vector-tile rendering in tiled_vector_layers with custom vector_styles + // It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced. + // Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles + + const dynamicTextProperty = makeProperty(new MockStyle({ min: 0, max: 100 }), null); + const mockMbMap = (new MockMbMap(undefined) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts index 22ea3067b1748..e8612388a5ae1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts @@ -20,7 +20,9 @@ export class DynamicTextProperty extends DynamicStyleProperty { + return new StaticTextProperty({ value }, VECTOR_STYLES.LABEL_TEXT); +}; + +describe('syncTextFieldWithMb', () => { + test('Should set with value', async () => { + const dynamicTextProperty = makeProperty('foo'); + const mockMbMap = (new MockMbMap() as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([['foobar', 'foo']]); + }); + + test('Should not clear when already cleared', async () => { + // This verifies a weird edge-case in mapbox-gl, where setting the `text-field` layout-property to null causes tiles to be invalidated. + // This triggers a refetch of the tile during panning and zooming + // This affects vector-tile rendering in tiled_vector_layers with custom vector_styles + // It does _not_ affect EMS, since that does not have a code-path where a `text-field` need to be resynced. + // Do not remove this logic without verifying that mapbox-gl does not re-issue tile-requests for previously requested tiles + + const dynamicTextProperty = makeProperty(''); + const mockMbMap = (new MockMbMap(undefined) as unknown) as MbMap; + + dynamicTextProperty.syncTextFieldWithMb('foobar', mockMbMap); + + // @ts-expect-error + expect(mockMbMap.getPaintPropertyCalls()).toEqual([]); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts index b0016106b8c31..fb05fa052db21 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts @@ -18,7 +18,9 @@ export class StaticTextProperty extends StaticStyleProperty if (this.getOptions().value.length) { mbMap.setLayoutProperty(mbLayerId, 'text-field', this.getOptions().value); } else { - mbMap.setLayoutProperty(mbLayerId, 'text-field', null); + if (typeof mbMap.getLayoutProperty(mbLayerId, 'text-field') !== 'undefined') { + mbMap.setLayoutProperty(mbLayerId, 'text-field', undefined); + } } } } diff --git a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts index 5f81a74ab03ce..a8bc5b9a821f0 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { Filter } from '../../../../../../src/plugins/data/public'; -import { TooltipFeature } from '../../../../../plugins/maps/common/descriptor_types'; +import type { TooltipFeature } from '../../../../../plugins/maps/common/descriptor_types'; export interface ITooltipProperty { getPropertyKey(): string; diff --git a/x-pack/plugins/maps/public/components/ems_file_select.tsx b/x-pack/plugins/maps/public/components/ems_file_select.tsx index 64ae57fc81dcf..3d23854efb4fb 100644 --- a/x-pack/plugins/maps/public/components/ems_file_select.tsx +++ b/x-pack/plugins/maps/public/components/ems_file_select.tsx @@ -10,7 +10,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiSelect } from '@el import { i18n } from '@kbn/i18n'; import { FileLayer } from '@elastic/ems-client'; -import { getEmsFileLayers } from '../meta'; +import { getEmsFileLayers } from '../util'; import { getEmsUnavailableMessage } from './ems_unavailable_message'; interface Props { diff --git a/x-pack/plugins/maps/public/components/geo_field_select.tsx b/x-pack/plugins/maps/public/components/geo_field_select.tsx new file mode 100644 index 0000000000000..0b04ec7146611 --- /dev/null +++ b/x-pack/plugins/maps/public/components/geo_field_select.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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 { EuiFormRow } from '@elastic/eui'; +import { SingleFieldSelect } from './single_field_select'; +import { IFieldType } from '../../../../../src/plugins/data/common'; + +interface Props { + value: string; + geoFields: IFieldType[]; + onChange: (geoFieldName?: string) => void; +} + +export function GeoFieldSelect(props: Props) { + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx index 78739731e14b6..04ae7af62fddc 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx @@ -98,9 +98,9 @@ export class AddTooltipFieldPopover extends Component { } _togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); }; _closePopover = () => { diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 622aeae3cbb87..525ba394ed503 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -16,7 +16,6 @@ import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { MBMap } from '../mb_map'; // @ts-expect-error import { WidgetOverlay } from '../widget_overlay'; -// @ts-expect-error import { ToolbarOverlay } from '../toolbar_overlay'; // @ts-expect-error import { LayerPanel } from '../layer_panel'; 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 5e4c3c9b1981f..66c9a2462736a 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 @@ -33,7 +33,7 @@ import { RawValue, ZOOM_PRECISION, } from '../../../common/constants'; -import { getGlyphUrl, isRetina } from '../../meta'; +import { getGlyphUrl, isRetina } from '../../util'; import { syncLayerOrder } from './sort_layers'; // @ts-expect-error import { removeOrphanedSourcesAndLayers, addSpritesheetToMap } from './utils'; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap index 3407bcfd4f845..506767fcd4706 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap @@ -35,7 +35,12 @@ exports[`Must zoom tools and draw filter tools 1`] = ` diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js deleted file mode 100644 index 6470718fc7e4a..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { connect } from 'react-redux'; -import { ToolbarOverlay } from './toolbar_overlay'; - -function mapStateToProps() { - return {}; -} - -const connectedToolbarOverlay = connect(mapStateToProps, null)(ToolbarOverlay); -export { connectedToolbarOverlay as ToolbarOverlay }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts new file mode 100644 index 0000000000000..d1008edfd572d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/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 { ToolbarOverlay } from './toolbar_overlay'; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.ts similarity index 62% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.ts index 3220f84967f16..8f7a3cf762a6b 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.ts @@ -5,33 +5,27 @@ * 2.0. */ +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { SetViewControl } from './set_view_control'; -import { setGotoWithCenter, closeSetView, openSetView } from '../../../actions'; +import { setGotoWithCenter } from '../../../actions'; import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors'; -import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; +import { MapStoreState } from '../../../reducers/store'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { return { settings: getMapSettings(state), - isSetViewOpen: getIsSetViewOpen(state), zoom: getMapZoom(state), center: getMapCenter(state), }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { - onSubmit: ({ lat, lon, zoom }) => { - dispatch(closeSetView()); + onSubmit: ({ lat, lon, zoom }: { lat: number; lon: number; zoom: number }) => { dispatch(setGotoWithCenter({ lat, lon, zoom })); }, - closeSetView: () => { - dispatch(closeSetView()); - }, - openSetView: () => { - dispatch(openSetView()); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx similarity index 69% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx index 21818476d6965..b657d6369f8aa 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { ChangeEvent, Component } from 'react'; import { EuiForm, EuiFormRow, @@ -19,57 +18,86 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -function getViewString(lat, lon, zoom) { - return `${lat},${lon},${zoom}`; +import { MapCenter } from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; + +export interface Props { + settings: MapSettings; + zoom: number; + center: MapCenter; + onSubmit: ({ lat, lon, zoom }: { lat: number; lon: number; zoom: number }) => void; } -export class SetViewControl extends Component { - state = {}; - - static getDerivedStateFromProps(nextProps, prevState) { - const nextView = getViewString(nextProps.center.lat, nextProps.center.lon, nextProps.zoom); - if (nextView !== prevState.prevView) { - return { - lat: nextProps.center.lat, - lon: nextProps.center.lon, - zoom: nextProps.zoom, - prevView: nextView, - }; - } +interface State { + isPopoverOpen: boolean; + lat: number | string; + lon: number | string; + zoom: number | string; +} - return null; - } +export class SetViewControl extends Component { + state: State = { + isPopoverOpen: false, + lat: 0, + lon: 0, + zoom: 0, + }; _togglePopover = () => { - if (this.props.isSetViewOpen) { - this.props.closeSetView(); + if (this.state.isPopoverOpen) { + this._closePopover(); return; } - this.props.openSetView(); + this.setState({ + lat: this.props.center.lat, + lon: this.props.center.lon, + zoom: this.props.zoom, + isPopoverOpen: true, + }); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); }; - _onLatChange = (evt) => { + _onLatChange = (evt: ChangeEvent) => { this._onChange('lat', evt); }; - _onLonChange = (evt) => { + _onLonChange = (evt: ChangeEvent) => { this._onChange('lon', evt); }; - _onZoomChange = (evt) => { + _onZoomChange = (evt: ChangeEvent) => { this._onChange('zoom', evt); }; - _onChange = (name, evt) => { + _onChange = (name: 'lat' | 'lon' | 'zoom', evt: ChangeEvent) => { const sanitizedValue = parseFloat(evt.target.value); + // @ts-expect-error this.setState({ [name]: isNaN(sanitizedValue) ? '' : sanitizedValue, }); }; - _renderNumberFormRow = ({ value, min, max, onChange, label, dataTestSubj }) => { + _renderNumberFormRow = ({ + value, + min, + max, + onChange, + label, + dataTestSubj, + }: { + value: string | number; + min: number; + max: number; + onChange: (evt: ChangeEvent) => void; + label: string; + dataTestSubj: string; + }) => { const isInvalid = value === '' || value > max || value < min; const error = isInvalid ? `Must be between ${min} and ${max}` : null; return { @@ -90,7 +118,8 @@ export class SetViewControl extends Component { _onSubmit = () => { const { lat, lon, zoom } = this.state; - this.props.onSubmit({ lat, lon, zoom }); + this._closePopover(); + this.props.onSubmit({ lat: lat as number, lon: lon as number, zoom: zoom as number }); }; _renderSetViewForm() { @@ -175,23 +204,11 @@ export class SetViewControl extends Component { })} /> } - isOpen={this.props.isSetViewOpen} - closePopover={this.props.closeSetView} + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} > {this._renderSetViewForm()} ); } } - -SetViewControl.propTypes = { - isSetViewOpen: PropTypes.bool.isRequired, - zoom: PropTypes.number.isRequired, - center: PropTypes.shape({ - lat: PropTypes.number.isRequired, - lon: PropTypes.number.isRequired, - }), - onSubmit: PropTypes.func.isRequired, - closeSetView: PropTypes.func.isRequired, - openSetView: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js deleted file mode 100644 index ceca3f5b7fdc1..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ /dev/null @@ -1,53 +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 } from '@elastic/eui'; -import { SetViewControl } from './set_view_control'; -import { ToolsControl } from './tools_control'; -import { FitToData } from './fit_to_data'; - -export class ToolbarOverlay extends React.Component { - _renderToolsControl() { - const { addFilters, geoFields, getFilterActions, getActionContext } = this.props; - if (!addFilters || !geoFields.length) { - return null; - } - - return ( - - - - ); - } - - render() { - return ( - - - - - - - - - - {this._renderToolsControl()} - - ); - } -} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx index a6d17819e2fea..d8ac971ae3983 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { Filter } from 'src/plugins/data/public'; jest.mock('../../kibana_services', () => { return { @@ -16,15 +17,25 @@ jest.mock('../../kibana_services', () => { }; }); -// @ts-ignore import { ToolbarOverlay } from './toolbar_overlay'; test('Must render zoom tools', async () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); test('Must zoom tools and draw filter tools', async () => { - const component = shallow( {}} geoFields={['coordinates']} />); + const geoFieldWithIndex = { + geoFieldName: 'myGeoFieldName', + geoFieldType: 'geo_point', + indexPatternTitle: 'myIndex', + indexPatternId: '1', + }; + const component = shallow( + {}} + geoFields={[geoFieldWithIndex]} + /> + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx new file mode 100644 index 0000000000000..c5208bc254fc8 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Filter } from 'src/plugins/data/public'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; +import { SetViewControl } from './set_view_control'; +import { ToolsControl } from './tools_control'; +import { FitToData } from './fit_to_data'; +import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; + +export interface Props { + addFilters?: ((filters: Filter[], actionId: string) => Promise) | null; + geoFields: GeoFieldWithIndex[]; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; +} + +export function ToolbarOverlay(props: Props) { + function renderToolsControl() { + const { addFilters, geoFields, getFilterActions, getActionContext } = props; + if (!addFilters || !geoFields.length) { + return null; + } + + return ( + + + + ); + } + + return ( + + + + + + + + + + {renderToolsControl()} + + ); +} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index b1944f8136709..a4ce76b702d13 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -11,7 +11,6 @@ import { EmbeddableFactoryDefinition, IContainer, } from '../../../../../src/plugins/embeddable/public'; -import '../index.scss'; import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts new file mode 100644 index 0000000000000..34a53be48a5cd --- /dev/null +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts @@ -0,0 +1,171 @@ +/* + * Copyright 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 { suggestEMSTermJoinConfig } from './ems_autosuggest'; +import { FORMAT_TYPE } from '../../common'; +import { FeatureCollection } from 'geojson'; + +class MockFileLayer { + private readonly _url: string; + private readonly _id: string; + private readonly _fields: Array<{ id: string }>; + + constructor(url: string, fields: Array<{ id: string }>) { + this._url = url; + this._id = url; + this._fields = fields; + } + getDefaultFormatUrl() { + return this._url; + } + + getFields() { + return this._fields; + } + + getDefaultFormatType() { + return FORMAT_TYPE.GEOJSON; + } + + hasId(id: string) { + return id === this._id; + } +} + +jest.mock('../util', () => { + return { + async getEmsFileLayers() { + return [ + new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]), + new MockFileLayer('zips', [{ id: 'zip' }]), + ]; + }, + async fetchGeoJson(url: string): Promise { + if (url === 'world_countries') { + return ({ + type: 'FeatureCollection', + features: [ + { properties: { iso2: 'CA', iso3: 'CAN' } }, + { properties: { iso2: 'US', iso3: 'USA' } }, + ], + } as unknown) as FeatureCollection; + } else if (url === 'zips') { + return ({ + type: 'FeatureCollection', + features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }], + } as unknown) as FeatureCollection; + } else { + throw new Error(`unrecognized mock url ${url}`); + } + }, + }; +}); + +describe('suggestEMSTermJoinConfig', () => { + test('no info provided', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({}); + expect(termJoinConfig).toBe(null); + }); + + describe('validate common column names', () => { + test('ecs region', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValuesColumnName: 'destination.geo.region_iso_code', + }); + expect(termJoinConfig).toEqual({ + layerId: 'administrative_regions_lvl2', + field: 'region_iso_code', + }); + }); + + test('ecs country', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValuesColumnName: 'country_iso_code', + }); + expect(termJoinConfig).toEqual({ + layerId: 'world_countries', + field: 'iso2', + }); + }); + + test('country', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValuesColumnName: 'Country_name', + }); + expect(termJoinConfig).toEqual({ + layerId: 'world_countries', + field: 'name', + }); + }); + + test('unknown name', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValuesColumnName: 'cntry', + }); + expect(termJoinConfig).toEqual(null); + }); + }); + + describe('validate well known formats', () => { + test('5-digit zip code', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValues: ['90201', 40204], + }); + expect(termJoinConfig).toEqual({ + layerId: 'usa_zip_codes', + field: 'zip', + }); + }); + + test('mismatch', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValues: ['90201', 'foobar'], + }); + expect(termJoinConfig).toEqual(null); + }); + }); + + describe('validate based on EMS data', () => { + test('Should validate with zip codes layer', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValues: ['40204', 40205], + emsLayerIds: ['world_countries', 'zips'], + }); + expect(termJoinConfig).toEqual({ + layerId: 'zips', + field: 'zip', + }); + }); + + test('Should not validate with faulty zip codes', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValues: ['40204', '00000'], + emsLayerIds: ['world_countries', 'zips'], + }); + expect(termJoinConfig).toEqual(null); + }); + + test('Should validate against countries', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValues: ['USA', 'USA', 'CAN'], + emsLayerIds: ['world_countries', 'zips'], + }); + expect(termJoinConfig).toEqual({ + layerId: 'world_countries', + field: 'iso3', + }); + }); + + test('Should not validate against missing countries', async () => { + const termJoinConfig = await suggestEMSTermJoinConfig({ + sampleValues: ['USA', 'BEL', 'CAN'], + emsLayerIds: ['world_countries', 'zips'], + }); + expect(termJoinConfig).toEqual(null); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts new file mode 100644 index 0000000000000..1d5c1529a004e --- /dev/null +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts @@ -0,0 +1,201 @@ +/* + * Copyright 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 { FileLayer } from '@elastic/ems-client'; +import { getEmsFileLayers, fetchGeoJson } from '../util'; +import { FORMAT_TYPE, emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common'; + +export interface SampleValuesConfig { + emsLayerIds?: string[]; + sampleValues?: Array; + sampleValuesColumnName?: string; +} + +export interface EMSTermJoinConfig { + layerId: string; + field: string; +} + +const wellKnownColumnNames = [ + { + regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country + emsConfig: { + layerId: emsWorldLayerId, + field: 'iso2', + }, + }, + { + regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region + emsConfig: { + layerId: emsRegionLayerId, + field: 'region_iso_code', + }, + }, + { + regex: /^country/i, // anything starting with country + emsConfig: { + layerId: emsWorldLayerId, + field: 'name', + }, + }, +]; + +const wellKnownColumnFormats = [ + { + regex: /(^\d{5}$)/i, // 5-digit zipcode + emsConfig: { + layerId: emsUsaZipLayerId, + field: 'zip', + }, + }, +]; + +interface UniqueMatch { + config: { layerId: string; field: string }; + count: number; +} + +export async function suggestEMSTermJoinConfig( + sampleValuesConfig: SampleValuesConfig +): Promise { + const matches: EMSTermJoinConfig[] = []; + + if (sampleValuesConfig.sampleValuesColumnName) { + matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName)); + } + + if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) { + if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) { + matches.push( + ...(await suggestByEMSLayerIds( + sampleValuesConfig.emsLayerIds, + sampleValuesConfig.sampleValues + )) + ); + } else { + matches.push(...suggestByValues(sampleValuesConfig.sampleValues)); + } + } + + const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => { + const found = accum.find((m) => { + return m.config.layerId === match.layerId && m.config.field === match.layerId; + }); + + if (found) { + found.count += 1; + } else { + accum.push({ + config: match, + count: 1, + }); + } + + return accum; + }, []); + + uniqMatches.sort((a, b) => { + return b.count - a.count; + }); + + return uniqMatches.length ? uniqMatches[0].config : null; +} + +function suggestByName(columnName: string): EMSTermJoinConfig[] { + const matches = wellKnownColumnNames.filter((wellknown) => { + return columnName.match(wellknown.regex); + }); + + return matches.map((m) => { + return m.emsConfig; + }); +} + +function suggestByValues(values: Array): EMSTermJoinConfig[] { + const matches = wellKnownColumnFormats.filter((wellknown) => { + for (let i = 0; i < values.length; i++) { + const value = values[i].toString(); + if (!value.match(wellknown.regex)) { + return false; + } + } + return true; + }); + + return matches.map((m) => { + return m.emsConfig; + }); +} + +function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean { + for (let i = 0; i < emsJson.features.length; i++) { + const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString(); + if (emsFieldValue.toString() === sampleValue) { + return true; + } + } + return false; +} + +function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array) { + for (let j = 0; j < sampleValues.length; j++) { + const sampleValue = sampleValues[j].toString(); + if (!existsInEMS(emsJson, emsFieldId, sampleValue)) { + return false; + } + } + return true; +} + +async function getMatchesForEMSLayer( + emsLayerId: string, + sampleValues: Array +): Promise { + const fileLayers: FileLayer[] = await getEmsFileLayers(); + const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) => + fl.hasId(emsLayerId) + ); + + if (!emsFileLayer) { + return []; + } + + const emsFields = emsFileLayer.getFields(); + const url = emsFileLayer.getDefaultFormatUrl(); + + try { + const emsJson = await fetchGeoJson( + url, + emsFileLayer.getDefaultFormatType() as FORMAT_TYPE, + 'data' + ); + const matches: EMSTermJoinConfig[] = []; + for (let f = 0; f < emsFields.length; f++) { + if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) { + matches.push({ + layerId: emsLayerId, + field: emsFields[f].id, + }); + } + } + return matches; + } catch (e) { + return []; + } +} + +async function suggestByEMSLayerIds( + emsLayerIds: string[], + values: Array +): Promise { + const matches = []; + for (const emsLayerId of emsLayerIds) { + const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values); + matches.push(...layerIdMathes); + } + return matches; +} diff --git a/x-pack/plugins/maps/public/ems_autosuggest/index.ts b/x-pack/plugins/maps/public/ems_autosuggest/index.ts new file mode 100644 index 0000000000000..86ed9e4fa70e1 --- /dev/null +++ b/x-pack/plugins/maps/public/ems_autosuggest/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 * from './ems_autosuggest'; diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 3e6cd8d14ad37..dc9cb2d594fe3 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -19,6 +19,8 @@ export const plugin: PluginInitializer = ( export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; -export { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; +export type { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; export { MapsStartApi } from './api'; + +export type { MapEmbeddable, MapEmbeddableInput } from './embeddable'; diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts index f7894085b15ac..3b1cb461c8779 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.ts @@ -56,6 +56,12 @@ export function getTermsFields(fields: IFieldType[]): IFieldType[] { }); } +export function getSortFields(fields: IFieldType[]): IFieldType[] { + return fields.filter((field) => { + return field.sortable && !indexPatterns.isNestedField(field); + }); +} + export function getAggregatableGeoFieldTypes(): string[] { const aggregatableFieldTypes = [ES_GEO_FIELD_TYPE.GEO_POINT]; if (getIsGoldPlus()) { diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 6fd14d8d42e18..e4b9397fab8e7 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -5,13 +5,12 @@ * 2.0. */ -import _ from 'lodash'; -import { CoreStart } from 'kibana/public'; +import type { CoreStart } from 'kibana/public'; import type { MapsEmsConfig } from '../../../../src/plugins/maps_ems/public'; -import { MapsConfigType } from '../config'; -import { MapsPluginStartDependencies } from './plugin'; -import { EMSSettings } from '../common/ems_settings'; -import { PaletteRegistry } from '../../../../src/plugins/charts/public'; +import type { MapsConfigType } from '../config'; +import type { MapsPluginStartDependencies } from './plugin'; +import type { EMSSettings } from '../common/ems_settings'; +import type { PaletteRegistry } from '../../../../src/plugins/charts/public'; let kibanaVersion: string; export const setKibanaVersion = (version: string) => (kibanaVersion = version); @@ -75,8 +74,22 @@ export const getEMSSettings = () => { export const getEmsTileLayerId = () => getKibanaCommonConfig().emsTileLayerId; -export const getRegionmapLayers = () => _.get(getKibanaCommonConfig(), 'regionmap.layers', []); -export const getTilemap = () => _.get(getKibanaCommonConfig(), 'tilemap', []); +export const getRegionmapLayers = () => { + const config = getKibanaCommonConfig(); + if (config.regionmap && config.regionmap.layers) { + return config.regionmap.layers; + } else { + return []; + } +}; +export const getTilemap = () => { + const config = getKibanaCommonConfig(); + if (config.tilemap) { + return config.tilemap; + } else { + return {}; + } +}; export const getShareService = () => pluginsStart.share; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 0bf604a26544b..3e5e2d54422d6 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -14,6 +14,7 @@ import { MapEmbeddableConfig, MapEmbeddableInput, MapEmbeddableOutput } from '.. import { SourceRegistryEntry } from '../classes/sources/source_registry'; import { LayerWizard } from '../classes/layers/layer_wizard_registry'; import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; +import type { EMSTermJoinConfig, SampleValuesConfig } from '../ems_autosuggest'; let loadModulesPromise: Promise; @@ -74,6 +75,7 @@ interface LazyLoadedMapModules { }) => LayerDescriptor | null; createBasemapLayerDescriptor: () => LayerDescriptor | null; createESSearchSourceLayerDescriptor: (params: CreateLayerDescriptorParams) => LayerDescriptor; + suggestEMSTermJoinConfig: (config: SampleValuesConfig) => Promise; } export async function lazyLoadMapModules(): Promise { @@ -94,6 +96,7 @@ export async function lazyLoadMapModules(): Promise { createRegionMapLayerDescriptor, createBasemapLayerDescriptor, createESSearchSourceLayerDescriptor, + suggestEMSTermJoinConfig, } = await import('./lazy'); resolve({ @@ -108,6 +111,7 @@ export async function lazyLoadMapModules(): Promise { createRegionMapLayerDescriptor, createBasemapLayerDescriptor, createESSearchSourceLayerDescriptor, + suggestEMSTermJoinConfig, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 0d908356b714d..e7f5df49527b7 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import '../../index.scss'; export * from '../../embeddable/map_embeddable'; export * from '../../kibana_services'; export { renderApp } from '../../render_app'; @@ -15,3 +16,4 @@ export { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_m export { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor'; export { createBasemapLayerDescriptor } from '../../classes/layers/create_basemap_layer_descriptor'; export { createLayerDescriptor as createESSearchSourceLayerDescriptor } from '../../classes/sources/es_search_source'; +export { suggestEMSTermJoinConfig } from '../../ems_autosuggest'; diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index a3a8b55745d84..194b4595c0c93 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { +import type { VisualizationsSetup, VisualizationStage, } from '../../../../src/plugins/visualizations/public'; -import { SavedObject } from '../../../../src/core/types/saved_objects'; -import { MapSavedObject } from '../common/map_saved_object_type'; +import type { SavedObject } from '../../../../src/core/types/saved_objects'; +import type { MapSavedObject } from '../common/map_saved_object_type'; import { APP_ID, APP_ICON, diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index be2e097c71dc5..ad8846bd48b60 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; -import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; -import { DashboardStart } from 'src/plugins/dashboard/public'; -import { +import type { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; +import type { UiActionsStart } from 'src/plugins/ui_actions/public'; +import type { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; +import type { DashboardStart } from 'src/plugins/dashboard/public'; +import type { AppMountParameters, CoreSetup, CoreStart, Plugin, PluginInitializerContext, - DEFAULT_APP_CATEGORIES, } from '../../../../src/core/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; // @ts-ignore import { MapView } from './inspector/views/map_view'; import { @@ -29,8 +29,8 @@ import { } from './kibana_services'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { getMapsVisTypeAlias } from './maps_vis_type_alias'; -import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; -import { +import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import type { VisualizationsSetup, VisualizationsStart, } from '../../../../src/plugins/visualizations/public'; @@ -43,28 +43,32 @@ import { } from './url_generator'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; -import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; +import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { MapsXPackConfig, MapsConfigType } from '../config'; import { getAppTitle } from '../common/i18n_getters'; import { lazyLoadMapModules } from './lazy_load_bundle'; -import { MapsStartApi } from './api'; -import { createLayerDescriptors, registerLayerWizard, registerSource } from './api'; -import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; -import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { + createLayerDescriptors, + registerLayerWizard, + registerSource, + MapsStartApi, + suggestEMSTermJoinConfig, +} from './api'; +import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import type { MapsEmsPluginSetup } from '../../../../src/plugins/maps_ems/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; -import { FileUploadPluginStart } from '../../file_upload/public'; -import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; -import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; +import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import type { FileUploadPluginStart } from '../../file_upload/public'; +import type { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; +import type { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getIsEnterprisePlus, registerLicensedFeatures, setLicensingPluginStart, } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; -import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; -import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import type { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; +import type { ChartsPluginStart } from '../../../../src/plugins/charts/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -178,6 +182,7 @@ export class MapsPlugin createLayerDescriptors, registerLayerWizard, registerSource, + suggestEMSTermJoinConfig, }; } } diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index 90dafa3afb67a..676ac6ce12efe 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -11,8 +11,6 @@ import { getMapsCapabilities } from '../kibana_services'; import { UPDATE_FLYOUT, - CLOSE_SET_VIEW, - OPEN_SET_VIEW, SET_IS_LAYER_TOC_OPEN, SET_FULL_SCREEN, SET_READ_ONLY, @@ -33,7 +31,6 @@ export type MapUiState = { isFullScreen: boolean; isReadOnly: boolean; isLayerTOCOpen: boolean; - isSetViewOpen: boolean; openTOCDetails: string[]; }; @@ -44,7 +41,6 @@ export const DEFAULT_MAP_UI_STATE = { isFullScreen: false, isReadOnly: !getMapsCapabilities().save, isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN, - isSetViewOpen: false, // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], @@ -55,10 +51,6 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { switch (action.type) { case UPDATE_FLYOUT: return { ...state, flyoutDisplay: action.display }; - case CLOSE_SET_VIEW: - return { ...state, isSetViewOpen: false }; - case OPEN_SET_VIEW: - return { ...state, isSetViewOpen: true }; case SET_IS_LAYER_TOC_OPEN: return { ...state, isLayerTOCOpen: action.isLayerTOCOpen }; case SET_FULL_SCREEN: diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 597cd8e9c4287..7e0aa59756876 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -201,7 +201,11 @@ export function getTopNavConfig({ options={tagSelector} /> ) : ( - + ); showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext); diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index dc34035c21b29..e5c83bd0f8f4a 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -10,7 +10,6 @@ import { MapStoreState } from '../reducers/store'; import { FLYOUT_STATE } from '../reducers/ui'; export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; diff --git a/x-pack/plugins/maps/public/url_generator.ts b/x-pack/plugins/maps/public/url_generator.ts index c82af369fe113..9f28b388c4756 100644 --- a/x-pack/plugins/maps/public/url_generator.ts +++ b/x-pack/plugins/maps/public/url_generator.ts @@ -6,17 +6,17 @@ */ import rison from 'rison-node'; -import { +import type { TimeRange, Filter, Query, - esFilters, QueryState, RefreshInterval, } from '../../../../src/plugins/data/public'; +import { esFilters } from '../../../../src/plugins/data/public'; import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; -import { LayerDescriptor } from '../common/descriptor_types'; +import type { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; +import type { LayerDescriptor } from '../common/descriptor_types'; import { INITIAL_LAYERS_KEY } from '../common/constants'; import { lazyLoadMapModules } from './lazy_load_bundle'; diff --git a/x-pack/plugins/maps/public/meta.test.js b/x-pack/plugins/maps/public/util.test.js similarity index 98% rename from x-pack/plugins/maps/public/meta.test.js rename to x-pack/plugins/maps/public/util.test.js index fc26bca48032f..47c3d77180077 100644 --- a/x-pack/plugins/maps/public/meta.test.js +++ b/x-pack/plugins/maps/public/util.test.js @@ -6,7 +6,7 @@ */ import { EMSClient } from '@elastic/ems-client'; -import { getEMSClient, getGlyphUrl } from './meta'; +import { getEMSClient, getGlyphUrl } from './util'; jest.mock('@elastic/ems-client'); diff --git a/x-pack/plugins/maps/public/meta.ts b/x-pack/plugins/maps/public/util.ts similarity index 73% rename from x-pack/plugins/maps/public/meta.ts rename to x-pack/plugins/maps/public/util.ts index 11dc033846222..2745f9274f119 100644 --- a/x-pack/plugins/maps/public/meta.ts +++ b/x-pack/plugins/maps/public/util.ts @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; import { EMSClient, FileLayer, TMSService } from '@elastic/ems-client'; +import { FeatureCollection } from 'geojson'; +// @ts-expect-error +import * as topojson from 'topojson-client'; +import _ from 'lodash'; import fetch from 'node-fetch'; import { @@ -16,6 +20,7 @@ import { EMS_GLYPHS_PATH, EMS_APP_NAME, FONTS_API_PATH, + FORMAT_TYPE, } from '../common/constants'; import { getHttp, @@ -113,3 +118,41 @@ export function getGlyphUrl(): string { export function isRetina(): boolean { return window.devicePixelRatio === 2; } + +export async function fetchGeoJson( + fetchUrl: string, + format: FORMAT_TYPE, + featureCollectionPath: string +): Promise { + let fetchedJson; + try { + const response = await fetch(fetchUrl); + if (!response.ok) { + throw new Error('Request failed'); + } + fetchedJson = await response.json(); + } catch (e) { + throw new Error( + i18n.translate('xpack.maps.util.requestFailedErrorMessage', { + defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`, + values: { fetchUrl }, + }) + ); + } + + if (format === FORMAT_TYPE.GEOJSON) { + return fetchedJson; + } + + if (format === FORMAT_TYPE.TOPOJSON) { + const features = _.get(fetchedJson, `objects.${featureCollectionPath}`); + return topojson.feature(fetchedJson, features); + } + + throw new Error( + i18n.translate('xpack.maps.util.formatErrorMessage', { + defaultMessage: `Unable to fetch vector shapes from url: {format}`, + values: { format }, + }) + ); +} diff --git a/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js b/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js index 268794b8a1bce..6e68608c75cef 100644 --- a/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js +++ b/x-pack/plugins/maps/server/sample_data/ecommerce_saved_objects.js @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { emsWorldLayerId } from '../../common'; const layerList = [ { @@ -29,7 +30,7 @@ const layerList = [ alpha: 1, sourceDescriptor: { type: 'EMS_FILE', - id: 'world_countries', + id: emsWorldLayerId, tooltipProperties: ['name', 'iso2'], }, visible: true, diff --git a/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js b/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js index 31f353fab09ab..86c6c14306faf 100644 --- a/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js +++ b/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { emsWorldLayerId } from '../../common'; const layerList = [ { @@ -29,7 +30,7 @@ const layerList = [ alpha: 0.5, sourceDescriptor: { type: 'EMS_FILE', - id: 'world_countries', + id: emsWorldLayerId, tooltipProperties: ['name', 'iso2'], }, visible: true, diff --git a/x-pack/plugins/maps_legacy_licensing/README.md b/x-pack/plugins/maps_legacy_licensing/README.md deleted file mode 100644 index 7c2ce84d848d4..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Tile Map Plugin - -This plugin provides access to the detailed tile map services from Elastic. - diff --git a/x-pack/plugins/maps_legacy_licensing/kibana.json b/x-pack/plugins/maps_legacy_licensing/kibana.json deleted file mode 100644 index 7a49e0aaa7be1..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "mapsLegacyLicensing", - "version": "8.0.0", - "kibanaVersion": "kibana", - "server": false, - "ui": true, - "requiredPlugins": ["licensing", "mapsEms"] -} diff --git a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts deleted file mode 100644 index f8118575cd6a2..0000000000000 --- a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { LicensingPluginSetup, ILicense } from '../../licensing/public'; -import { IServiceSettings, MapsEmsPluginSetup } from '../../../../src/plugins/maps_ems/public'; - -/** - * These are the interfaces with your public contracts. You should export these - * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. - * @public - */ - -export interface MapsLegacyLicensingSetupDependencies { - licensing: LicensingPluginSetup; - mapsEms: MapsEmsPluginSetup; -} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MapsLegacyLicensingStartDependencies {} - -export type MapsLegacyLicensingSetup = ReturnType; -export type MapsLegacyLicensingStart = ReturnType; - -export class MapsLegacyLicensing - implements Plugin { - public setup(core: CoreSetup, plugins: MapsLegacyLicensingSetupDependencies) { - const { licensing, mapsEms } = plugins; - if (licensing) { - licensing.license$.subscribe(async (license: ILicense) => { - const serviceSettings: IServiceSettings = await mapsEms.getServiceSettings(); - const { uid, isActive } = license; - if (isActive && license.hasAtLeast('basic')) { - serviceSettings.setQueryParams({ license: uid || '' }); - serviceSettings.disableZoomMessage(); - } else { - serviceSettings.setQueryParams({ license: '' }); - serviceSettings.enableZoomMessage(); - } - }); - } - } - - public start(core: CoreStart, plugins: MapsLegacyLicensingStartDependencies) {} -} diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index ac21954118e50..c15aa8f414fb1 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -5,9 +5,11 @@ * 2.0. */ -export { HitsTotalRelation, SearchResponse7, HITS_TOTAL_RELATION } from './types/es_client'; +export { ES_CLIENT_TOTAL_HITS_RELATION } from './types/es_client'; export { ChartData } from './types/field_histograms'; export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; +export { isPopulatedObject } from './util/object_utils'; +export { isRuntimeMappings } from './util/runtime_field_utils'; export { composeValidators, patternValidator } from './util/validators'; export { extractErrorMessage } from './util/errors'; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 8686e3d64037e..d9632f4d4a83b 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -6,6 +6,8 @@ */ import Boom from '@hapi/boom'; +import { RuntimeMappings } from './fields'; + import { EsErrorBody } from '../util/errors'; import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; import { DATA_FRAME_TASK_STATE } from '../constants/data_frame_analytics'; @@ -74,6 +76,7 @@ export interface DataFrameAnalyticsConfig { source: { index: IndexName | IndexName[]; query?: any; + runtime_mappings?: RuntimeMappings; }; analysis: AnalysisConfig; analyzed_fields: { diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts index f6db736db2519..249b3c150a082 100644 --- a/x-pack/plugins/ml/common/types/es_client.ts +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -5,33 +5,24 @@ * 2.0. */ -import type { SearchResponse, ShardsResponse } from 'elasticsearch'; +import { estypes } from '@elastic/elasticsearch'; + import { buildEsQuery } from '../../../../../src/plugins/data/common/es_query/es_query'; import type { DslQuery } from '../../../../../src/plugins/data/common/es_query/kuery'; import type { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; -export const HITS_TOTAL_RELATION = { +import { isPopulatedObject } from '../util/object_utils'; + +export function isMultiBucketAggregate(arg: unknown): arg is estypes.MultiBucketAggregate { + return isPopulatedObject(arg, ['buckets']); +} + +export const ES_CLIENT_TOTAL_HITS_RELATION: Record< + Uppercase, + estypes.TotalHitsRelation +> = { EQ: 'eq', GTE: 'gte', } as const; -export type HitsTotalRelation = typeof HITS_TOTAL_RELATION[keyof typeof HITS_TOTAL_RELATION]; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7Hits { - hits: SearchResponse['hits']['hits']; - max_score: number; - total: { - value: number; - relation: HitsTotalRelation; - }; -} -export interface SearchResponse7 { - took: number; - timed_out: boolean; - _scroll_id?: string; - _shards: ShardsResponse; - hits: SearchResponse7Hits; - aggregations?: any; -} export type InfluencersFilterQuery = ReturnType | DslQuery | JsonObject; diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts index 964ce8c325783..111c8432dd439 100644 --- a/x-pack/plugins/ml/common/types/feature_importance.ts +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -88,15 +88,11 @@ export function isRegressionTotalFeatureImportance( export function isClassificationFeatureImportanceBaseline( baselineData: any ): baselineData is ClassificationFeatureImportanceBaseline { - return ( - isPopulatedObject(baselineData) && - baselineData.hasOwnProperty('classes') && - Array.isArray(baselineData.classes) - ); + return isPopulatedObject(baselineData, ['classes']) && Array.isArray(baselineData.classes); } export function isRegressionFeatureImportanceBaseline( baselineData: any ): baselineData is RegressionFeatureImportanceBaseline { - return isPopulatedObject(baselineData) && baselineData.hasOwnProperty('baseline'); + return isPopulatedObject(baselineData, ['baseline']); } diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index f9f7f8fc7ead6..8dfe9d111ed38 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -109,8 +109,8 @@ export interface AggCardinality { export type RollupFields = Record]>; // Replace this with import once #88995 is merged -const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { type: RuntimeType; diff --git a/x-pack/plugins/ml/common/util/object_utils.test.ts b/x-pack/plugins/ml/common/util/object_utils.test.ts new file mode 100644 index 0000000000000..8e4196ed4d826 --- /dev/null +++ b/x-pack/plugins/ml/common/util/object_utils.test.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 { isPopulatedObject } from './object_utils'; + +describe('object_utils', () => { + describe('isPopulatedObject()', () => { + it('does not allow numbers', () => { + expect(isPopulatedObject(0)).toBe(false); + }); + it('does not allow strings', () => { + expect(isPopulatedObject('')).toBe(false); + }); + it('does not allow null', () => { + expect(isPopulatedObject(null)).toBe(false); + }); + it('does not allow an empty object', () => { + expect(isPopulatedObject({})).toBe(false); + }); + it('allows an object with an attribute', () => { + expect(isPopulatedObject({ attribute: 'value' })).toBe(true); + }); + it('does not allow an object with a non-existing required attribute', () => { + expect(isPopulatedObject({ attribute: 'value' }, ['otherAttribute'])).toBe(false); + }); + it('allows an object with an existing required attribute', () => { + expect(isPopulatedObject({ attribute: 'value' }, ['attribute'])).toBe(true); + }); + it('allows an object with two existing required attributes', () => { + expect( + isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ + 'attribute1', + 'attribute2', + ]) + ).toBe(true); + }); + it('does not allow an object with two required attributes where one does not exist', () => { + expect( + isPopulatedObject({ attribute1: 'value1', attribute2: 'value2' }, [ + 'attribute1', + 'otherAttribute', + ]) + ).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/object_utils.ts b/x-pack/plugins/ml/common/util/object_utils.ts index 4bbd0c1c2810f..537ee9202b4de 100644 --- a/x-pack/plugins/ml/common/util/object_utils.ts +++ b/x-pack/plugins/ml/common/util/object_utils.ts @@ -5,6 +5,32 @@ * 2.0. */ -export const isPopulatedObject = >(arg: any): arg is T => { - return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0; +/* + * A type guard to check record like object structures. + * + * Examples: + * - `isPopulatedObject({...})` + * Limits type to Record + * + * - `isPopulatedObject({...}, ['attribute'])` + * Limits type to Record<'attribute', unknown> + * + * - `isPopulatedObject({...})` + * Limits type to a record with keys of the given interface. + * Note that you might want to add keys from the interface to the + * array of requiredAttributes to satisfy runtime requirements. + * Otherwise you'd just satisfy TS requirements but might still + * run into runtime issues. + */ +export const isPopulatedObject = ( + arg: unknown, + requiredAttributes: U[] = [] +): arg is Record => { + return ( + typeof arg === 'object' && + arg !== null && + Object.keys(arg).length > 0 && + (requiredAttributes.length === 0 || + requiredAttributes.every((d) => ({}.hasOwnProperty.call(arg, d)))) + ); }; diff --git a/x-pack/plugins/ml/common/util/runtime_field_utils.test.ts b/x-pack/plugins/ml/common/util/runtime_field_utils.test.ts new file mode 100644 index 0000000000000..1b5e3e18b14f6 --- /dev/null +++ b/x-pack/plugins/ml/common/util/runtime_field_utils.test.ts @@ -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 { isRuntimeField, isRuntimeMappings } from './runtime_field_utils'; + +describe('ML runtime field utils', () => { + describe('isRuntimeField()', () => { + it('does not allow numbers', () => { + expect(isRuntimeField(1)).toBe(false); + }); + it('does not allow null', () => { + expect(isRuntimeField(null)).toBe(false); + }); + it('does not allow arrays', () => { + expect(isRuntimeField([])).toBe(false); + }); + it('does not allow empty objects', () => { + expect(isRuntimeField({})).toBe(false); + }); + it('does not allow objects with non-matching attributes', () => { + expect(isRuntimeField({ someAttribute: 'someValue' })).toBe(false); + expect(isRuntimeField({ type: 'wrong-type' })).toBe(false); + expect(isRuntimeField({ type: 'keyword', someAttribute: 'some value' })).toBe(false); + }); + it('allows objects with type attribute only', () => { + expect(isRuntimeField({ type: 'keyword' })).toBe(true); + }); + it('allows objects with both type and script attributes', () => { + expect(isRuntimeField({ type: 'keyword', script: 'some script' })).toBe(true); + }); + }); + + describe('isRuntimeMappings()', () => { + it('does not allow numbers', () => { + expect(isRuntimeMappings(1)).toBe(false); + }); + it('does not allow null', () => { + expect(isRuntimeMappings(null)).toBe(false); + }); + it('does not allow arrays', () => { + expect(isRuntimeMappings([])).toBe(false); + }); + it('does not allow empty objects', () => { + expect(isRuntimeMappings({})).toBe(false); + }); + it('does not allow objects with non-object inner structure', () => { + expect(isRuntimeMappings({ someAttribute: 'someValue' })).toBe(false); + }); + it('does not allow objects with objects with unsupported inner structure', () => { + expect(isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: 'someValue' })).toBe( + false + ); + expect( + isRuntimeMappings({ + fieldName1: { type: 'keyword' }, + fieldName2: { type: 'keyword', someAttribute: 'some value' }, + }) + ).toBe(false); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: 1234 }, + }) + ).toBe(false); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: { someAttribute: 'some value' } }, + }) + ).toBe(false); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: { source: 1234 } }, + }) + ).toBe(false); + }); + + it('allows object with most basic runtime mapping', () => { + expect(isRuntimeMappings({ fieldName: { type: 'keyword' } })).toBe(true); + }); + it('allows object with multiple most basic runtime mappings', () => { + expect( + isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: { type: 'keyword' } }) + ).toBe(true); + }); + it('allows object with runtime mappings including scripts', () => { + expect( + isRuntimeMappings({ + fieldName1: { type: 'keyword' }, + fieldName2: { type: 'keyword', script: 'some script as script' }, + }) + ).toBe(true); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: { source: 'some script as source' } }, + }) + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/runtime_field_utils.ts b/x-pack/plugins/ml/common/util/runtime_field_utils.ts index 340f374747253..6d911ecd5d3cb 100644 --- a/x-pack/plugins/ml/common/util/runtime_field_utils.ts +++ b/x-pack/plugins/ml/common/util/runtime_field_utils.ts @@ -6,23 +6,19 @@ */ import { isPopulatedObject } from './object_utils'; -import { - RUNTIME_FIELD_TYPES, - RuntimeType, -} from '../../../../../src/plugins/data/common/index_patterns'; +import { RUNTIME_FIELD_TYPES } from '../../../../../src/plugins/data/common'; import type { RuntimeField, RuntimeMappings } from '../types/fields'; +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + export function isRuntimeField(arg: unknown): arg is RuntimeField { return ( - isPopulatedObject(arg) && - ((Object.keys(arg).length === 1 && arg.hasOwnProperty('type')) || - (Object.keys(arg).length === 2 && - arg.hasOwnProperty('type') && - arg.hasOwnProperty('script') && + ((isPopulatedObject(arg, ['type']) && Object.keys(arg).length === 1) || + (isPopulatedObject(arg, ['type', 'script']) && + Object.keys(arg).length === 2 && (typeof arg.script === 'string' || - (isPopulatedObject(arg.script) && + (isPopulatedObject(arg.script, ['source']) && Object.keys(arg.script).length === 1 && - arg.script.hasOwnProperty('source') && typeof arg.script.source === 'string')))) && RUNTIME_FIELD_TYPES.includes(arg.type as RuntimeType) ); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 312776f0d6a07..d3e58c4d7bb0d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -49,9 +49,8 @@ import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; -import type { RuntimeField } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { RuntimeMappings } from '../../../../common/types/fields'; -import { isPopulatedObject } from '../../../../common/util/object_utils'; +import { RuntimeMappings, RuntimeField } from '../../../../common/types/fields'; +import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; export const INIT_MAX_COLUMNS = 10; export const COLUMN_CHART_DEFAULT_VISIBILITY_ROWS_THRESHOLED = 10000; @@ -94,34 +93,36 @@ export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): str /** * Return a map of runtime_mappings for each of the index pattern field provided * to provide in ES search queries - * @param indexPatternFields * @param indexPattern - * @param clonedRuntimeMappings + * @param RuntimeMappings */ -export const getRuntimeFieldsMapping = ( - indexPatternFields: string[] | undefined, +export function getCombinedRuntimeMappings( indexPattern: IndexPattern | undefined, - clonedRuntimeMappings?: RuntimeMappings -) => { - if (!Array.isArray(indexPatternFields) || indexPattern === undefined) return {}; - const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields; - let combinedRuntimeMappings: RuntimeMappings = {}; - - if (isPopulatedObject(ipRuntimeMappings)) { - indexPatternFields.forEach((ipField) => { - if (ipRuntimeMappings.hasOwnProperty(ipField)) { - // @ts-expect-error - combinedRuntimeMappings[ipField] = ipRuntimeMappings[ipField]; + runtimeMappings?: RuntimeMappings +): RuntimeMappings | undefined { + let combinedRuntimeMappings = {}; + + // And runtime field mappings defined by index pattern + if (indexPattern) { + const computedFields = indexPattern?.getComputedFields(); + if (computedFields?.runtimeFields !== undefined) { + const indexPatternRuntimeMappings = computedFields.runtimeFields; + if (isRuntimeMappings(indexPatternRuntimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...indexPatternRuntimeMappings }; } - }); + } } - if (isPopulatedObject(clonedRuntimeMappings)) { - combinedRuntimeMappings = { ...combinedRuntimeMappings, ...clonedRuntimeMappings }; + + // Use runtime field mappings defined inline from API + // and override fields with same name from index pattern + if (isRuntimeMappings(runtimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...runtimeMappings }; } - return Object.keys(combinedRuntimeMappings).length > 0 - ? { runtime_mappings: combinedRuntimeMappings } - : {}; -}; + + if (isRuntimeMappings(combinedRuntimeMappings)) { + return combinedRuntimeMappings; + } +} export interface FieldTypes { [key: string]: ES_FIELD_TYPES; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index be37e381d1bae..481ff432e0156 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -10,7 +10,7 @@ export { getDataGridSchemaFromESFieldType, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, - getRuntimeFieldsMapping, + getCombinedRuntimeMappings, multiColumnSortFactory, showDataGridColumnChartErrorMessageToast, useRenderCellValue, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index 649968f176e18..0af8972f18558 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -7,6 +7,7 @@ import { Dispatch, SetStateAction } from 'react'; +import { estypes } from '@elastic/elasticsearch'; import { EuiDataGridCellValueElementProps, EuiDataGridPaginationProps, @@ -15,7 +16,6 @@ import { } from '@elastic/eui'; import { Dictionary } from '../../../../common/types/common'; -import { HitsTotalRelation } from '../../../../common/types/es_client'; import { ChartData } from '../../../../common/types/field_histograms'; import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; @@ -27,7 +27,7 @@ export type DataGridItem = Record; // `undefined` is used to indicate a non-initialized state. export type ChartsVisible = boolean | undefined; -export type RowCountRelation = HitsTotalRelation | undefined; +export type RowCountRelation = estypes.TotalHitsRelation | undefined; export type IndexPagination = Pick; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index 31a72a776223e..e62f2eb2f003b 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; -import { HITS_TOTAL_RELATION } from '../../../../common/types/es_client'; +import { ES_CLIENT_TOTAL_HITS_RELATION } from '../../../../common/types/es_client'; import { ChartData } from '../../../../common/types/field_histograms'; import { INDEX_STATUS } from '../../data_frame_analytics/common'; @@ -146,7 +146,7 @@ export const useDataGrid = ( if (chartsVisible === undefined && rowCount > 0 && rowCountRelation !== undefined) { setChartsVisible( rowCount <= COLUMN_CHART_DEFAULT_VISIBILITY_ROWS_THRESHOLED && - rowCountRelation !== HITS_TOTAL_RELATION.GTE + rowCountRelation !== ES_CLIENT_TOTAL_HITS_RELATION.GTE ); } }, [chartsVisible, rowCount, rowCountRelation]); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts index 4788254e97d1e..15979751f40d6 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -30,6 +30,7 @@ export function chartLoaderProvider(mlResultsService: MlResultsService) { job.data_counts.earliest_record_timestamp, job.data_counts.latest_record_timestamp, intervalMs, + job.datafeed_config.runtime_mappings, // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options job.datafeed_config.indices_options ); diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 4e9fd3baebe7b..bc76020d19649 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -7,6 +7,8 @@ import React, { useMemo, useEffect, useState, FC } from 'react'; +import { estypes } from '@elastic/elasticsearch'; + import { EuiCallOut, EuiComboBox, @@ -22,10 +24,13 @@ import { import { i18n } from '@kbn/i18n'; +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; import { extractErrorMessage } from '../../../../common'; +import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils'; import { stringHash } from '../../../../common/util/string_utils'; -import type { SearchResponse7 } from '../../../../common/types/es_client'; +import { RuntimeMappings } from '../../../../common/types/fields'; import type { ResultsSearchQuery } from '../../data_frame_analytics/common/analytics'; +import { getCombinedRuntimeMappings } from '../../components/data_grid'; import { useMlApiContext } from '../../contexts/kibana'; @@ -83,6 +88,8 @@ export interface ScatterplotMatrixProps { color?: string; legendType?: LegendType; searchQuery?: ResultsSearchQuery; + runtimeMappings?: RuntimeMappings; + indexPattern?: IndexPattern; } export const ScatterplotMatrix: FC = ({ @@ -92,6 +99,8 @@ export const ScatterplotMatrix: FC = ({ color, legendType, searchQuery, + runtimeMappings, + indexPattern, }) => { const { esSearch } = useMlApiContext(); @@ -184,7 +193,10 @@ export const ScatterplotMatrix: FC = ({ } : searchQuery; - const resp: SearchResponse7 = await esSearch({ + const combinedRuntimeMappings = + indexPattern && getCombinedRuntimeMappings(indexPattern, runtimeMappings); + + const resp: estypes.SearchResponse = await esSearch({ index, body: { fields: queryFields, @@ -192,13 +204,16 @@ export const ScatterplotMatrix: FC = ({ query, from: 0, size: fetchSize, + ...(isRuntimeMappings(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), }, }); if (!options.didCancel) { const items = resp.hits.hits .map((d) => - getProcessedFields(d.fields, (key: string) => + getProcessedFields(d.fields ?? {}, (key: string) => key.startsWith(`${resultsField}.feature_importance`) ) ) diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts index 1ce03119c161d..12a4d9257c5e7 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -6,7 +6,7 @@ */ // @ts-ignore -import { compile } from 'vega-lite/build-es5/vega-lite'; +import { compile } from 'vega-lite/build/vega-lite'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index 9d8e3b6546327..f10ccb6e92a90 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -7,7 +7,7 @@ // There is still an issue with Vega Lite's typings with the strict mode Kibana is using. // @ts-ignore -import type { TopLevelSpec } from 'vega-lite/build-es5/vega-lite'; +import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx b/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx index 7774def574b69..5064cf6435227 100644 --- a/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx +++ b/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx @@ -9,11 +9,11 @@ import React, { useMemo, useEffect, FC } from 'react'; // There is still an issue with Vega Lite's typings with the strict mode Kibana is using. // @ts-ignore -import type { TopLevelSpec } from 'vega-lite/build-es5/vega-lite'; +import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; // There is still an issue with Vega Lite's typings with the strict mode Kibana is using. // @ts-ignore -import { compile } from 'vega-lite/build-es5/vega-lite'; +import { compile } from 'vega-lite/build/vega-lite'; import { parse, View, Warn } from 'vega'; import { Handler } from 'vega-tooltip'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index d0bcbd2ff63b4..88f403cdf0c44 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SearchResponse7 } from '../../../../common/types/es_client'; +import type { estypes } from '@elastic/elasticsearch'; import { extractErrorMessage } from '../../../../common/util/errors'; import { EsSorting, UseDataGridReturnType, getProcessedFields } from '../../components/data_grid'; @@ -51,7 +51,7 @@ export const getIndexData = async ( const { pageIndex, pageSize } = pagination; // TODO: remove results_field from `fields` when possible - const resp: SearchResponse7 = await ml.esSearch({ + const resp: estypes.SearchResponse = await ml.esSearch({ index: jobConfig.dest.index, body: { fields: ['*'], @@ -64,11 +64,15 @@ export const getIndexData = async ( }); if (!options.didCancel) { - setRowCount(resp.hits.total.value); - setRowCountRelation(resp.hits.total.relation); + setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); + setRowCountRelation( + typeof resp.hits.total === 'number' + ? ('eq' as estypes.TotalHitsRelation) + : resp.hits.total.relation + ); setTableItems( resp.hits.hits.map((d) => - getProcessedFields(d.fields, (key: string) => + getProcessedFields(d.fields ?? {}, (key: string) => key.startsWith(`${jobConfig.dest.results_field}.feature_importance`) ) ) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx index 3b9c84e2fa51a..710fd49f72fb6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx @@ -13,12 +13,17 @@ import { ConfigurationStepDetails } from './configuration_step_details'; import { ConfigurationStepForm } from './configuration_step_form'; import { ANALYTICS_STEPS } from '../../page'; -export const ConfigurationStep: FC = ({ +export interface ConfigurationStepProps extends CreateAnalyticsStepProps { + isClone: boolean; +} + +export const ConfigurationStep: FC = ({ actions, state, setCurrentStep, step, stepActivated, + isClone, }) => { const showForm = step === ANALYTICS_STEPS.CONFIGURATION; const showDetails = step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true; @@ -30,7 +35,12 @@ export const ConfigurationStep: FC = ({ return ( {showForm && ( - + )} {showDetails && } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 36d3de1376373..1046f1a8c3e92 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBadge, - EuiCallOut, EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, @@ -18,11 +17,11 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; +import { debounce, cloneDeep } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { getCombinedRuntimeMappings } from '../../../../../components/data_grid/common'; import { ANALYSIS_CONFIG_TYPE, @@ -31,13 +30,18 @@ import { FieldSelectionItem, } from '../../../../common/analytics'; import { getScatterplotMatrixLegendType } from '../../../../common/get_scatterplot_matrix_legend_type'; -import { CreateAnalyticsStepProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { RuntimeMappings as RuntimeMappingsType } from '../../../../../../../common/types/fields'; +import { + isRuntimeMappings, + isRuntimeField, +} from '../../../../../../../common/util/runtime_field_utils'; +import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { Messages } from '../shared'; import { DEFAULT_MODEL_MEMORY_LIMIT, State, } from '../../../analytics_management/hooks/use_create_analytics_form/state'; -import { shouldAddAsDepVarOption } from './form_options_validation'; +import { handleExplainErrorMessage, shouldAddAsDepVarOption } from './form_options_validation'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { ANALYTICS_STEPS } from '../../page'; @@ -55,6 +59,18 @@ import { ExplorationQueryBarProps } from '../../../analytics_exploration/compone import { Query } from '../../../../../../../../../../src/plugins/data/common/query'; import { ScatterplotMatrix } from '../../../../../components/scatterplot_matrix'; +import { RuntimeMappings } from '../runtime_mappings'; +import { ConfigurationStepProps } from './configuration_step'; + +const runtimeMappingKey = 'runtime_mapping'; +const notIncludedReason = 'field not in includes list'; +const requiredFieldsErrorText = i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', + { + defaultMessage: + 'At least one field must be included in the analysis in addition to the dependent variable.', + } +); function getIndexDataQuery(savedSearchQuery: SavedSearchQuery, jobConfigQuery: any) { // Return `undefined` if savedSearchQuery itself is `undefined`, meaning it hasn't been initialized yet. @@ -65,18 +81,23 @@ function getIndexDataQuery(savedSearchQuery: SavedSearchQuery, jobConfigQuery: a return savedSearchQuery !== null ? savedSearchQuery : jobConfigQuery; } -const requiredFieldsErrorText = i18n.translate( - 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', - { - defaultMessage: - 'At least one field must be included in the analysis in addition to the dependent variable.', - } -); - -const maxRuntimeFieldsDisplayCount = 5; +function getRuntimeDepVarOptions(jobType: AnalyticsJobType, runtimeMappings: RuntimeMappingsType) { + const runtimeOptions: EuiComboBoxOptionOption[] = []; + Object.keys(runtimeMappings).forEach((id) => { + const field = runtimeMappings[id]; + if (isRuntimeField(field) && shouldAddAsDepVarOption(id, field.type, jobType)) { + runtimeOptions.push({ + label: id, + key: `runtime_mapping_${id}`, + }); + } + }); + return runtimeOptions; +} -export const ConfigurationStepForm: FC = ({ +export const ConfigurationStepForm: FC = ({ actions, + isClone, state, setCurrentStep, }) => { @@ -100,7 +121,7 @@ export const ConfigurationStepForm: FC = ({ >(); const { setEstimatedModelMemoryLimit, setFormState } = actions; - const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; + const { cloneJob, estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; const firstUpdate = useRef(true); const { dependentVariable, @@ -111,10 +132,22 @@ export const ConfigurationStepForm: FC = ({ modelMemoryLimit, previousJobType, requiredFieldsError, + runtimeMappings, + previousRuntimeMapping, + runtimeMappingsUpdated, sourceIndex, trainingPercent, useEstimatedMml, } = form; + + const isJobTypeWithDepVar = + jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; + const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === ''; + const hasBasicRequiredFields = jobType !== undefined; + const hasRequiredAnalysisFields = + (isJobTypeWithDepVar && dependentVariable !== '') || + jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; + const [query, setQuery] = useState({ query: jobConfigQueryString ?? '', language: SEARCH_QUERY_LANGUAGE.KUERY, @@ -132,7 +165,8 @@ export const ConfigurationStepForm: FC = ({ const indexData = useIndexData( currentIndexPattern, getIndexDataQuery(savedSearchQuery, jobConfigQuery), - toastNotifications + toastNotifications, + runtimeMappings ); const indexPreviewProps = { @@ -141,11 +175,6 @@ export const ConfigurationStepForm: FC = ({ toastNotifications, }; - const isJobTypeWithDepVar = - jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; - - const dependentVariableEmpty = isJobTypeWithDepVar && dependentVariable === ''; - const isStepInvalid = dependentVariableEmpty || jobType === undefined || @@ -155,20 +184,23 @@ export const ConfigurationStepForm: FC = ({ unsupportedFieldsError !== undefined || fetchingExplainData; - const loadDepVarOptions = async (formState: State['form']) => { + const loadDepVarOptions = async ( + formState: State['form'], + runtimeOptions: EuiComboBoxOptionOption[] = [] + ) => { setLoadingDepVarOptions(true); setMaxDistinctValuesError(undefined); try { if (currentIndexPattern !== undefined) { const depVarOptions = []; - let depVarUpdate = dependentVariable; + let depVarUpdate = formState.dependentVariable; // Get fields and filter for supported types for job type const { fields } = newJobCapsService; let resetDependentVariable = true; for (const field of fields) { - if (shouldAddAsDepVarOption(field, jobType)) { + if (shouldAddAsDepVarOption(field.id, field.type, jobType)) { depVarOptions.push({ label: field.id, }); @@ -179,10 +211,21 @@ export const ConfigurationStepForm: FC = ({ } } + if ( + isRuntimeMappings(formState.runtimeMappings) && + Object.keys(formState.runtimeMappings).includes(form.dependentVariable) + ) { + resetDependentVariable = false; + depVarOptions.push({ + label: form.dependentVariable, + key: `runtime_mapping_${form.dependentVariable}`, + }); + } + if (resetDependentVariable) { depVarUpdate = ''; } - setDependentVariableOptions(depVarOptions); + setDependentVariableOptions([...runtimeOptions, ...depVarOptions]); setLoadingDepVarOptions(false); setDependentVariableFetchFail(false); setFormState({ dependentVariable: depVarUpdate }); @@ -209,8 +252,23 @@ export const ConfigurationStepForm: FC = ({ if (jobTypeChanged) { setLoadingFieldOptions(true); } + // Ensure runtime field is in 'includes' table if it is set as dependent variable + const depVarIsRuntimeField = + isJobTypeWithDepVar && + runtimeMappings && + Object.keys(runtimeMappings).includes(dependentVariable) && + includes.length > 0 && + includes.includes(dependentVariable) === false; + let formToUse = form; + + if (depVarIsRuntimeField) { + formToUse = cloneDeep(form); + formToUse.includes = [...includes, dependentVariable]; + } - const { success, expectedMemory, fieldSelection, errorMessage } = await fetchExplainData(form); + const { success, expectedMemory, fieldSelection, errorMessage } = await fetchExplainData( + formToUse + ); if (success) { if (shouldUpdateEstimatedMml) { @@ -226,53 +284,33 @@ export const ConfigurationStepForm: FC = ({ setFieldOptionsFetchFail(false); setMaxDistinctValuesError(undefined); setUnsupportedFieldsError(undefined); - setIncludesTableItems(fieldSelection ? fieldSelection : []); setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + includes: formToUse.includes, }); + setIncludesTableItems(fieldSelection ? fieldSelection : []); } else { setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + includes: formToUse.includes, }); } setFetchingExplainData(false); } else { - let maxDistinctValuesErrorMessage; - let unsupportedFieldsErrorMessage; - if ( - jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - (errorMessage.includes('must have at most') || errorMessage.includes('must have at least')) - ) { - maxDistinctValuesErrorMessage = errorMessage; - } else if ( - errorMessage.includes('status_exception') && - errorMessage.includes('unsupported type') - ) { - unsupportedFieldsErrorMessage = errorMessage; - } else if ( - errorMessage.includes('status_exception') && - errorMessage.includes('Unable to estimate memory usage as no documents') - ) { - toastNotifications.addWarning( - i18n.translate('xpack.ml.dataframe.analytics.create.allDocsMissingFieldsErrorMessage', { - defaultMessage: `Unable to estimate memory usage. There are mapped fields for source index [{index}] that do not exist in any indexed documents. You will have to switch to the JSON editor for explicit field selection and include only fields that exist in indexed documents.`, - values: { - index: sourceIndex, - }, - }) - ); - } else { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage', - { - defaultMessage: 'An error occurred fetching analysis fields data.', - } - ), - text: errorMessage, - }); + const { + maxDistinctValuesErrorMessage, + unsupportedFieldsErrorMessage, + toastNotificationDanger, + toastNotificationWarning, + } = handleExplainErrorMessage(errorMessage, sourceIndex, jobType); + + if (toastNotificationDanger) { + toastNotifications.addDanger(toastNotificationDanger); + } + if (toastNotificationWarning) { + toastNotifications.addWarning(toastNotificationWarning); } const fallbackModelMemoryLimit = @@ -304,17 +342,126 @@ export const ConfigurationStepForm: FC = ({ useEffect(() => { if (isJobTypeWithDepVar) { - loadDepVarOptions(form); + const indexPatternRuntimeFields = getCombinedRuntimeMappings(currentIndexPattern); + let runtimeOptions; + + if (indexPatternRuntimeFields) { + runtimeOptions = getRuntimeDepVarOptions(jobType, indexPatternRuntimeFields); + } + + loadDepVarOptions(form, runtimeOptions); } }, [jobType]); - useEffect(() => { - const hasBasicRequiredFields = jobType !== undefined; + const handleRuntimeUpdate = useCallback(async () => { + if (runtimeMappingsUpdated) { + // Update dependent variable options + let resetDepVar = false; + if (isJobTypeWithDepVar) { + const filteredOptions = dependentVariableOptions.filter((option) => { + if (option.label === dependentVariable && option.key?.includes(runtimeMappingKey)) { + resetDepVar = true; + } + return !option.key?.includes(runtimeMappingKey); + }); + // Runtime mappings have been removed + if (runtimeMappings === undefined && runtimeMappingsUpdated === true) { + setDependentVariableOptions(filteredOptions); + } else if (runtimeMappings) { + // add to filteredOptions if it's the type supported + const runtimeOptions = getRuntimeDepVarOptions(jobType, runtimeMappings); + setDependentVariableOptions([...filteredOptions, ...runtimeOptions]); + } + } - const hasRequiredAnalysisFields = - (isJobTypeWithDepVar && dependentVariable !== '') || - jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; + // Update includes - remove previous runtime mappings then add supported runtime fields to includes + const updatedIncludes = includes.filter((field) => { + const isRemovedRuntimeField = previousRuntimeMapping && previousRuntimeMapping[field]; + return !isRemovedRuntimeField; + }); + if (resetDepVar) { + setFormState({ + dependentVariable: '', + includes: updatedIncludes, + }); + setIncludesTableItems( + includesTableItems.filter(({ name }) => { + const isRemovedRuntimeField = previousRuntimeMapping && previousRuntimeMapping[name]; + return !isRemovedRuntimeField; + }) + ); + } + + if (!resetDepVar && hasBasicRequiredFields && hasRequiredAnalysisFields) { + const formCopy = cloneDeep(form); + // When switching back to step ensure runtime field is in 'includes' table if it is set as dependent variable + const depVarIsRuntimeField = + isJobTypeWithDepVar && + runtimeMappings && + Object.keys(runtimeMappings).includes(dependentVariable) && + formCopy.includes.length > 0 && + formCopy.includes.includes(dependentVariable) === false; + + formCopy.includes = depVarIsRuntimeField + ? [...updatedIncludes, dependentVariable] + : updatedIncludes; + + const { success, fieldSelection, errorMessage } = await fetchExplainData(formCopy); + if (success) { + // update the field selection table + const hasRequiredFields = fieldSelection.some( + (field) => field.is_included === true && field.is_required === false + ); + let updatedFieldSelection; + // Update field selection to select supported runtime fields by default. Add those fields to 'includes'. + if (isRuntimeMappings(runtimeMappings)) { + updatedFieldSelection = fieldSelection.map((field) => { + if ( + runtimeMappings[field.name] !== undefined && + field.is_included === false && + field.reason?.includes(notIncludedReason) + ) { + updatedIncludes.push(field.name); + field.is_included = true; + } + return field; + }); + } + setIncludesTableItems(updatedFieldSelection ? updatedFieldSelection : fieldSelection); + setMaxDistinctValuesError(undefined); + setUnsupportedFieldsError(undefined); + setFormState({ + includes: updatedIncludes, + requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, + }); + } else { + const { + maxDistinctValuesErrorMessage, + unsupportedFieldsErrorMessage, + toastNotificationDanger, + toastNotificationWarning, + } = handleExplainErrorMessage(errorMessage, sourceIndex, jobType); + + if (toastNotificationDanger) { + toastNotifications.addDanger(toastNotificationDanger); + } + if (toastNotificationWarning) { + toastNotifications.addWarning(toastNotificationWarning); + } + + setMaxDistinctValuesError(maxDistinctValuesErrorMessage); + setUnsupportedFieldsError(unsupportedFieldsErrorMessage); + } + } + } + }, [JSON.stringify(runtimeMappings)]); + + useEffect(() => { + handleRuntimeUpdate(); + }, [JSON.stringify(runtimeMappings)]); + + useEffect(() => { if (hasBasicRequiredFields && hasRequiredAnalysisFields) { debouncedGetExplainData(); } @@ -324,15 +471,6 @@ export const ConfigurationStepForm: FC = ({ }; }, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]); - const unsupportedRuntimeFields = useMemo( - () => - currentIndexPattern.fields - .getAll() - .filter((f) => f.runtimeField) - .map((f) => `'${f.displayName}'`), - [currentIndexPattern.fields] - ); - const scatterplotMatrixProps = useMemo( () => ({ color: isJobTypeWithDepVar ? dependentVariable : undefined, @@ -342,6 +480,8 @@ export const ConfigurationStepForm: FC = ({ index: currentIndexPattern.title, legendType: getScatterplotMatrixLegendType(jobType), searchQuery: jobConfigQuery, + runtimeMappings, + indexPattern: currentIndexPattern, }), [ currentIndexPattern.title, @@ -388,6 +528,7 @@ export const ConfigurationStepForm: FC = ({ /> )} + {((isClone && cloneJob) || !isClone) && } @@ -476,11 +617,11 @@ export const ConfigurationStepForm: FC = ({ singleSelection={true} options={dependentVariableOptions} selectedOptions={dependentVariable ? [{ label: dependentVariable }] : []} - onChange={(selectedOptions) => + onChange={(selectedOptions) => { setFormState({ dependentVariable: selectedOptions[0].label || '', - }) - } + }); + }} isClearable={false} isInvalid={dependentVariable === ''} data-test-subj={`mlAnalyticsCreateJobWizardDependentVariableSelect${ @@ -500,35 +641,6 @@ export const ConfigurationStepForm: FC = ({ > - {Array.isArray(unsupportedRuntimeFields) && unsupportedRuntimeFields.length > 0 && ( - <> - - 0 ? ( - - ) : ( - '' - ), - unsupportedRuntimeFields: unsupportedRuntimeFields - .slice(0, maxRuntimeFieldsDisplayCount) - .join(', '), - }} - /> - - - - )} { - if (field.id === EVENT_RATE_FIELD_ID) return false; +export const shouldAddAsDepVarOption = ( + fieldId: string, + fieldType: ES_FIELD_TYPES | RuntimeType, + jobType: AnalyticsJobType +) => { + if (fieldId === EVENT_RATE_FIELD_ID) return false; - const isBasicNumerical = BASIC_NUMERICAL_TYPES.has(field.type); + const isBasicNumerical = BASIC_NUMERICAL_TYPES.has(fieldType as ES_FIELD_TYPES); const isSupportedByClassification = - isBasicNumerical || CATEGORICAL_TYPES.has(field.type) || field.type === ES_FIELD_TYPES.BOOLEAN; + isBasicNumerical || CATEGORICAL_TYPES.has(fieldType) || fieldType === ES_FIELD_TYPES.BOOLEAN; if (jobType === ANALYSIS_CONFIG_TYPE.REGRESSION) { - return isBasicNumerical || EXTENDED_NUMERICAL_TYPES.has(field.type); + return isBasicNumerical || EXTENDED_NUMERICAL_TYPES.has(fieldType as ES_FIELD_TYPES); } if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) return isSupportedByClassification; }; + +export const handleExplainErrorMessage = ( + errorMessage: string, + sourceIndex: string, + jobType: AnalyticsJobType +) => { + let maxDistinctValuesErrorMessage; + let unsupportedFieldsErrorMessage; + let toastNotificationWarning; + let toastNotificationDanger; + if ( + jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + (errorMessage.includes('must have at most') || errorMessage.includes('must have at least')) + ) { + maxDistinctValuesErrorMessage = errorMessage; + } else if ( + errorMessage.includes('status_exception') && + errorMessage.includes('unsupported type') + ) { + unsupportedFieldsErrorMessage = errorMessage; + } else if ( + errorMessage.includes('status_exception') && + errorMessage.includes('Unable to estimate memory usage as no documents') + ) { + toastNotificationWarning = i18n.translate( + 'xpack.ml.dataframe.analytics.create.allDocsMissingFieldsErrorMessage', + { + defaultMessage: `Unable to estimate memory usage. There are mapped fields for source index [{index}] that do not exist in any indexed documents. You will have to switch to the JSON editor for explicit field selection and include only fields that exist in indexed documents.`, + values: { + index: sourceIndex, + }, + } + ); + } else { + toastNotificationDanger = { + title: i18n.translate('xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage', { + defaultMessage: 'An error occurred fetching analysis fields data.', + }), + text: errorMessage, + }; + } + + return { + maxDistinctValuesErrorMessage, + unsupportedFieldsErrorMessage, + toastNotificationDanger, + toastNotificationWarning, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index cca9855ec1b69..443e2cfacbb5e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -48,7 +48,7 @@ const jobDetails: JobDetails = { }, [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: { helpText: i18n.translate('xpack.ml.dataframe.analytics.create.classificationHelpText', { - defaultMessage: 'Classification predicts labels of data points in the data set.', + defaultMessage: 'Classification predicts classes of data points in the data set.', }), icon: 'classificationJob', title: i18n.translate('xpack.ml.dataframe.analytics.create.classificationTitle', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/index.ts new file mode 100644 index 0000000000000..8b93ddaa4a26a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/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 { RuntimeMappings } from './runtime_mappings'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx new file mode 100644 index 0000000000000..d9f1d78c302fd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { XJsonMode } from '@kbn/ace'; +import { RuntimeField } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { useMlContext } from '../../../../../contexts/ml'; +import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; +import { XJson } from '../../../../../../../../../../src/plugins/es_ui_shared/public'; +import { getCombinedRuntimeMappings } from '../../../../../components/data_grid/common'; +import { isPopulatedObject } from '../../../../../../../common/util/object_utils'; +import { RuntimeMappingsEditor } from './runtime_mappings_editor'; + +const advancedEditorsSidebarWidth = '220px'; +const COPY_TO_CLIPBOARD_RUNTIME_MAPPINGS = i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.indexPreview.copyRuntimeMappingsClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the runtime mappings to the clipboard.', + } +); + +const { useXJsonMode } = XJson; +const xJsonMode = new XJsonMode(); + +interface Props { + actions: CreateAnalyticsFormProps['actions']; + state: CreateAnalyticsFormProps['state']; +} + +type RuntimeMappings = Record; + +export const RuntimeMappings: FC = ({ actions, state }) => { + const [isRuntimeMappingsEditorEnabled, setIsRuntimeMappingsEditorEnabled] = useState( + false + ); + const [ + isRuntimeMappingsEditorApplyButtonEnabled, + setIsRuntimeMappingsEditorApplyButtonEnabled, + ] = useState(false); + const [ + advancedEditorRuntimeMappingsLastApplied, + setAdvancedEditorRuntimeMappingsLastApplied, + ] = useState(); + const [advancedEditorRuntimeMappings, setAdvancedEditorRuntimeMappings] = useState(); + + const { setFormState } = actions; + const { jobType, previousRuntimeMapping, runtimeMappings } = state.form; + + const { + convertToJson, + setXJson: setAdvancedRuntimeMappingsConfig, + xJson: advancedRuntimeMappingsConfig, + } = useXJsonMode(runtimeMappings || ''); + + const mlContext = useMlContext(); + const { currentIndexPattern } = mlContext; + + const applyChanges = () => { + const removeRuntimeMappings = advancedRuntimeMappingsConfig === ''; + const parsedRuntimeMappings = removeRuntimeMappings + ? undefined + : JSON.parse(advancedRuntimeMappingsConfig); + const prettySourceConfig = removeRuntimeMappings + ? '' + : JSON.stringify(parsedRuntimeMappings, null, 2); + const previous = + previousRuntimeMapping === undefined && runtimeMappings === undefined + ? parsedRuntimeMappings + : runtimeMappings; + setFormState({ + runtimeMappings: parsedRuntimeMappings, + runtimeMappingsUpdated: true, + previousRuntimeMapping: previous, + }); + setAdvancedEditorRuntimeMappings(prettySourceConfig); + setAdvancedEditorRuntimeMappingsLastApplied(prettySourceConfig); + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + }; + + // If switching to KQL after updating via editor - reset search + const toggleEditorHandler = (reset = false) => { + if (reset === true) { + setFormState({ runtimeMappingsUpdated: false }); + } + if (isRuntimeMappingsEditorEnabled === false) { + setAdvancedEditorRuntimeMappingsLastApplied(advancedEditorRuntimeMappings); + } + + setIsRuntimeMappingsEditorEnabled(!isRuntimeMappingsEditorEnabled); + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + }; + + useEffect(function getInitialRuntimeMappings() { + const combinedRuntimeMappings = getCombinedRuntimeMappings( + currentIndexPattern, + runtimeMappings + ); + + if (combinedRuntimeMappings) { + setAdvancedRuntimeMappingsConfig(JSON.stringify(combinedRuntimeMappings, null, 2)); + setFormState({ + runtimeMappings: combinedRuntimeMappings, + }); + } + }, []); + + return ( + <> + + + + + {isPopulatedObject(runtimeMappings) ? ( + + {Object.keys(runtimeMappings).join(',')} + + ) : ( + + )} + + {isRuntimeMappingsEditorEnabled && ( + <> + + + + )} + + + + + + + + toggleEditorHandler()} + data-test-subj="mlDataFrameAnalyticsRuntimeMappingsEditorSwitch" + /> + + + + {(copy: () => void) => ( + + )} + + + + + + {isRuntimeMappingsEditorEnabled && ( + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.advancedRuntimeMappingsEditorHelpText', + { + defaultMessage: + 'The advanced editor allows you to edit the runtime mappings of the source.', + } + )} + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.advancedSourceEditorApplyButtonText', + { + defaultMessage: 'Apply changes', + } + )} + + + )} + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.tsx new file mode 100644 index 0000000000000..70544cc14ba08 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings_editor.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 { isEqual } from 'lodash'; +import React, { memo, FC } from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isRuntimeMappings } from '../../../../../../../common/util/runtime_field_utils'; + +interface Props { + convertToJson: (data: string) => string; + setAdvancedRuntimeMappingsConfig: React.Dispatch; + setIsRuntimeMappingsEditorApplyButtonEnabled: React.Dispatch>; + advancedEditorRuntimeMappingsLastApplied: string | undefined; + advancedRuntimeMappingsConfig: string; + xJsonMode: any; +} + +export const RuntimeMappingsEditor: FC = memo( + ({ + convertToJson, + xJsonMode, + setAdvancedRuntimeMappingsConfig, + setIsRuntimeMappingsEditorApplyButtonEnabled, + advancedEditorRuntimeMappingsLastApplied, + advancedRuntimeMappingsConfig, + }) => { + return ( + { + setAdvancedRuntimeMappingsConfig(d); + + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorRuntimeMappingsLastApplied === d) { + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + return; + } + + // Enable Apply button so user can remove previously created runtime field + if (d === '') { + setIsRuntimeMappingsEditorApplyButtonEnabled(true); + return; + } + + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + const parsedJson = JSON.parse(convertToJson(d)); + setIsRuntimeMappingsEditorApplyButtonEnabled(isRuntimeMappings(parsedJson)); + } catch (e) { + setIsRuntimeMappingsEditorApplyButtonEnabled(false); + } + }} + setOptions={{ + fontSize: '12px', + }} + theme="textmate" + aria-label={i18n.translate( + 'xpack.ml.dataframe.analytics.createWizard.runtimeMappings.advancedEditorAriaLabel', + { + defaultMessage: 'Advanced runtime editor', + } + )} + /> + ); + }, + (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps)) +); + +function pickProps(props: Props) { + return [props.advancedEditorRuntimeMappingsLastApplied, props.advancedRuntimeMappingsConfig]; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index ecda624c71d98..f48f4a62f5a7d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -5,19 +5,23 @@ * 2.0. */ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { estypes } from '@elastic/elasticsearch'; import { EuiDataGridColumn } from '@elastic/eui'; - import { CoreSetup } from 'src/core/public'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils'; +import { RuntimeMappings, RuntimeField } from '../../../../../../common/types/fields'; +import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms'; import { DataLoader } from '../../../../datavisualizer/index_based/data_loader'; import { getFieldType, getDataGridSchemaFromKibanaFieldType, + getDataGridSchemaFromESFieldType, getFieldsFromKibanaIndexPattern, showDataGridColumnChartErrorMessageToast, useDataGrid, @@ -25,32 +29,51 @@ import { EsSorting, UseIndexDataReturnType, getProcessedFields, + getCombinedRuntimeMappings, } from '../../../../components/data_grid'; -import type { SearchResponse7 } from '../../../../../../common/types/es_client'; import { extractErrorMessage } from '../../../../../../common/util/errors'; import { INDEX_STATUS } from '../../../common/analytics'; import { ml } from '../../../../services/ml_api_service'; -import { getRuntimeFieldsMapping } from '../../../../components/data_grid/common'; -type IndexSearchResponse = SearchResponse7; +type IndexSearchResponse = estypes.SearchResponse; + +interface MLEuiDataGridColumn extends EuiDataGridColumn { + isRuntimeFieldColumn?: boolean; +} + +function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) { + return Object.keys(runtimeMappings).map((id) => { + const field = runtimeMappings[id]; + const schema = getDataGridSchemaFromESFieldType(field.type as RuntimeField['type']); + return { id, schema, isExpandable: schema !== 'boolean', isRuntimeFieldColumn: true }; + }); +} export const useIndexData = ( indexPattern: IndexPattern, query: Record | undefined, - toastNotifications: CoreSetup['notifications']['toasts'] + toastNotifications: CoreSetup['notifications']['toasts'], + runtimeMappings?: RuntimeMappings ): UseIndexDataReturnType => { const indexPatternFields = useMemo(() => getFieldsFromKibanaIndexPattern(indexPattern), [ indexPattern, ]); - // EuiDataGrid State - const columns: EuiDataGridColumn[] = [ + const [columns, setColumns] = useState([ ...indexPatternFields.map((id) => { const field = indexPattern.fields.getByName(id); - const schema = getDataGridSchemaFromKibanaFieldType(field); - return { id, schema, isExpandable: schema !== 'boolean' }; + const isRuntimeFieldColumn = field?.runtimeField !== undefined; + const schema = isRuntimeFieldColumn + ? getDataGridSchemaFromESFieldType(field?.type as RuntimeField['type']) + : getDataGridSchemaFromKibanaFieldType(field); + return { + id, + schema, + isExpandable: schema !== 'boolean', + isRuntimeFieldColumn, + }; }), - ]; + ]); const dataGrid = useDataGrid(columns); @@ -75,6 +98,8 @@ export const useIndexData = ( setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); + const combinedRuntimeMappings = getCombinedRuntimeMappings(indexPattern, runtimeMappings); + const sort: EsSorting = sortingColumns.reduce((s, column) => { s[column.id] = { order: column.direction }; return s; @@ -88,16 +113,43 @@ export const useIndexData = ( fields: ['*'], _source: false, ...(Object.keys(sort).length > 0 ? { sort } : {}), - ...getRuntimeFieldsMapping(indexPatternFields, indexPattern), + ...(isRuntimeMappings(combinedRuntimeMappings) + ? { runtime_mappings: combinedRuntimeMappings } + : {}), }, }; try { const resp: IndexSearchResponse = await ml.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields)); - setRowCount(resp.hits.total.value); - setRowCountRelation(resp.hits.total.relation); + const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + + if (isRuntimeMappings(runtimeMappings)) { + // remove old runtime field from columns + const updatedColumns = columns.filter((col) => col.isRuntimeFieldColumn === false); + setColumns([ + ...updatedColumns, + ...(combinedRuntimeMappings ? getRuntimeFieldColumns(combinedRuntimeMappings) : []), + ]); + } else { + setColumns([ + ...indexPatternFields.map((id) => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { + id, + schema, + isExpandable: schema !== 'boolean', + isRuntimeFieldColumn: field?.runtimeField !== undefined, + }; + }), + ]); + } + setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); + setRowCountRelation( + typeof resp.hits.total === 'number' + ? ('eq' as estypes.TotalHitsRelation) + : resp.hits.total.relation + ); setTableItems(docs); setStatus(INDEX_STATUS.LOADED); } catch (e) { @@ -111,13 +163,18 @@ export const useIndexData = ( getIndexData(); } // custom comparison - }, [indexPattern.title, indexPatternFields, JSON.stringify([query, pagination, sortingColumns])]); + }, [ + indexPattern.title, + indexPatternFields, + JSON.stringify([query, pagination, sortingColumns, runtimeMappings]), + ]); const dataLoader = useMemo(() => new DataLoader(indexPattern, toastNotifications), [ indexPattern, ]); const fetchColumnChartsData = async function (fieldHistogramsQuery: Record) { + const combinedRuntimeMappings = getCombinedRuntimeMappings(indexPattern, runtimeMappings); try { const columnChartsData = await dataLoader.loadFieldHistograms( columns @@ -126,7 +183,9 @@ export const useIndexData = ( fieldName: cT.id, type: getFieldType(cT.schema), })), - fieldHistogramsQuery + fieldHistogramsQuery, + DEFAULT_SAMPLER_SHARD_SIZE, + combinedRuntimeMappings ); dataGrid.setColumnCharts(columnChartsData); } catch (e) { @@ -142,7 +201,7 @@ export const useIndexData = ( }, [ dataGrid.chartsVisible, indexPattern.title, - JSON.stringify([query, dataGrid.visibleColumns]), + JSON.stringify([query, dataGrid.visibleColumns, runtimeMappings]), ]); const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 8fd0ae86d240c..830870cf1ca74 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -104,6 +104,7 @@ export const Page: FC = ({ jobId }) => { children: ( = ({ jobConfig, jobStatus, se

@@ -320,7 +320,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx index e482c89a96dc0..e9a6925476b02 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx @@ -7,7 +7,7 @@ // There is still an issue with Vega Lite's typings with the strict mode Kibana is using. // @ts-ignore -import type { TopLevelSpec } from 'vega-lite/build-es5/vega-lite'; +import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; import { euiPaletteColorBlind, euiPaletteGray } from '@elastic/eui'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx index 654af03d102e5..d67473d9d3220 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section_results.tsx @@ -18,7 +18,7 @@ import { isClassificationAnalysis, isRegressionAnalysis, } from '../../../../../../../common/util/analytics_utils'; -import { HITS_TOTAL_RELATION } from '../../../../../../../common/types/es_client'; +import { ES_CLIENT_TOTAL_HITS_RELATION } from '../../../../../../../common/types/es_client'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { useColorRange, ColorRangeLegend } from '../../../../../components/color_range_legend'; @@ -77,7 +77,7 @@ const getResultsSectionHeaderItems = ( defaultMessage="Total docs" /> ), - value: `${rowCountRelation === HITS_TOTAL_RELATION.GTE ? '>' : ''}${rowCount}`, + value: `${rowCountRelation === ES_CLIENT_TOTAL_HITS_RELATION.GTE ? '>' : ''}${rowCount}`, }, ...(colorRange !== undefined ? [ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index 2ce6e7ac0e33d..c661c40958bc0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -289,6 +289,11 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo match_all: {}, }, }, + runtime_mappings: { + optional: true, + formKey: 'runtimeMappings', + defaultValue: undefined, + }, _source: { optional: true, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 36c66a76c68f6..5065aefd921da 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; +import { memoize, isEqual } from 'lodash'; // @ts-ignore import numeral from '@elastic/numeral'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; @@ -470,7 +470,11 @@ export function reducer(state: State, action: Action): State { let disableSwitchToForm = false; try { resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); - disableSwitchToForm = isAdvancedConfig(resultJobConfig); + const runtimeMappingsChanged = + state.form.runtimeMappings && + resultJobConfig.source.runtime_mappings && + !isEqual(state.form.runtimeMappings, resultJobConfig.source.runtime_mappings); + disableSwitchToForm = isAdvancedConfig(resultJobConfig) || runtimeMappingsChanged; } catch (e) { return { ...state, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 7addbd273b725..22efe6f9eb3eb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { RuntimeMappings } from '../../../../../../../common/types/fields'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; +import { isRuntimeMappings } from '../../../../../../../common/util/runtime_field_utils'; import { defaultSearchQuery, getAnalysisType } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; @@ -94,6 +96,9 @@ export interface State { requiredFieldsError: string | undefined; randomizeSeed: undefined | number; resultsField: undefined | string; + runtimeMappings: undefined | RuntimeMappings; + runtimeMappingsUpdated: boolean; + previousRuntimeMapping: undefined | RuntimeMappings; softTreeDepthLimit: undefined | number; softTreeDepthTolerance: undefined | number; sourceIndex: EsIndexName; @@ -171,6 +176,9 @@ export const getInitialState = (): State => ({ requiredFieldsError: undefined, randomizeSeed: undefined, resultsField: undefined, + runtimeMappings: undefined, + runtimeMappingsUpdated: false, + previousRuntimeMapping: undefined, softTreeDepthLimit: undefined, softTreeDepthTolerance: undefined, sourceIndex: '', @@ -212,6 +220,9 @@ export const getJobConfigFromFormState = ( ? formState.sourceIndex.split(',').map((d) => d.trim()) : formState.sourceIndex, query: formState.jobConfigQuery, + ...(isRuntimeMappings(formState.runtimeMappings) + ? { runtime_mappings: formState.runtimeMappings } + : {}), }, dest: { index: formState.destinationIndex, @@ -340,6 +351,7 @@ export function getFormStateFromJobConfig( sourceIndex: Array.isArray(analyticsJobConfig.source.index) ? analyticsJobConfig.source.index.join(',') : analyticsJobConfig.source.index, + runtimeMappings: analyticsJobConfig.source.runtime_mappings, modelMemoryLimit: analyticsJobConfig.model_memory_limit, maxNumThreads: analyticsJobConfig.max_num_threads, includes: analyticsJobConfig.analyzed_fields?.includes ?? [], diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index 38b9aa2ce29f2..0da7d3d6b63d8 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -110,14 +110,15 @@ export class DataLoader { async loadFieldHistograms( fields: FieldHistogramRequestConfig[], query: string | SavedSearchQuery, - samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE + samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE, + editorRuntimeMappings?: RuntimeMappings ): Promise { const stats = await ml.getVisualizerFieldHistograms({ indexPatternTitle: this._indexPatternTitle, query, fields, samplerShardSize, - runtimeMappings: this._runtimeMappings, + runtimeMappings: editorRuntimeMappings || this._runtimeMappings, }); return stats; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index ddd2aa3619472..9c8f34260def0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -128,6 +128,7 @@ export class ChartLoader { start: number, end: number, intervalMs: number, + runtimeMappings?: RuntimeMappings, indicesOptions?: IndicesOptions ): Promise { if (this._timeFieldName !== '') { @@ -138,6 +139,7 @@ export class ChartLoader { start, end, intervalMs * 3, + runtimeMappings, indicesOptions ); if (resp.error !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts index bfed2d811e206..5995224ef3254 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts @@ -22,8 +22,10 @@ interface Response { export function filterRuntimeMappings(job: Job, datafeed: Datafeed): Response { if ( - datafeed.runtime_mappings === undefined || - isPopulatedObject(datafeed.runtime_mappings) === false + !( + isPopulatedObject(datafeed, ['runtime_mappings']) && + isPopulatedObject(datafeed.runtime_mappings) + ) ) { return { runtime_mappings: {}, @@ -83,7 +85,7 @@ function findFieldsInJob(job: Job, datafeed: Datafeed) { return [...usedFields]; } -function findFieldsInAgg(obj: Record) { +function findFieldsInAgg(obj: Record) { const fields: string[] = []; Object.entries(obj).forEach(([key, val]) => { if (isPopulatedObject(val)) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index 916a25271c63b..a4d9293e9369d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -22,6 +22,7 @@ import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { mlJobService } from '../../../../../../services/job_service'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; import { isPopulatedObject } from '../../../../../../../../common/util/object_utils'; +import { isMultiBucketAggregate } from '../../../../../../../../common/types/es_client'; export const DatafeedPreview: FC<{ combinedJob: CombinedJob | null; @@ -67,7 +68,10 @@ export const DatafeedPreview: FC<{ // the first item under aggregations can be any name if (isPopulatedObject(resp.aggregations)) { const accessor = Object.keys(resp.aggregations)[0]; - data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT); + const aggregate = resp.aggregations[accessor]; + if (isMultiBucketAggregate(aggregate)) { + data = aggregate.buckets.slice(0, ML_DATA_PREVIEW_COUNT); + } } setPreviewJsonString(JSON.stringify(data, null, 2)); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx index f3396a95738a6..c553da93a4bc0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx @@ -55,6 +55,7 @@ export const CategorizationDetectorsSummary: FC = () => { jobCreator.start, jobCreator.end, chartInterval.getInterval().asMilliseconds(), + jobCreator.runtimeMappings ?? undefined, // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index b57fd45019abe..7e7b919f677ab 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -48,6 +48,7 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) jobCreator.start, jobCreator.end, chartInterval.getInterval().asMilliseconds(), + jobCreator.runtimeMappings ?? undefined, // @ts-expect-error @elastic/elasticsearch Datafeed is missing indices_options jobCreator.datafeedConfig.indices_options ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 271898654ca49..69fe66b4ca190 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -44,6 +44,8 @@ import { JobId } from '../../../../../common/types/anomaly_detection_jobs'; import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; +import { isPopulatedObject } from '../../../../../common/util/object_utils'; +import { RuntimeMappings } from '../../../../../common/types/fields'; export interface ModuleJobUI extends ModuleJob { datafeedResult?: DatafeedResponse; @@ -133,10 +135,12 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { timeRange: TimeRange ): Promise => { if (useFullIndexData) { + const runtimeMappings = indexPattern.getComputedFields().runtimeFields as RuntimeMappings; const { start, end } = await ml.getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, query: combinedQuery, + ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); return { start: start.epoch, diff --git a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts index 59b6860cb65b7..72de5d003d4b8 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_explorer_charts_service.ts @@ -90,11 +90,7 @@ export interface SeriesConfigWithMetadata extends SeriesConfig { } export const isSeriesConfigWithMetadata = (arg: unknown): arg is SeriesConfigWithMetadata => { - return ( - isPopulatedObject(arg) && - {}.hasOwnProperty.call(arg, 'bucketSpanSeconds') && - {}.hasOwnProperty.call(arg, 'detectorLabel') - ); + return isPopulatedObject(arg, ['bucketSpanSeconds', 'detectorLabel']); }; interface ChartRange { diff --git a/x-pack/plugins/ml/public/application/services/job_service.d.ts b/x-pack/plugins/ml/public/application/services/job_service.d.ts index 544d346341591..ceadca12f8757 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/job_service.d.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import { estypes } from '@elastic/elasticsearch'; + import { TimeRange } from 'src/plugins/data/common/query/timefilter/types'; import { CombinedJob, Datafeed, Job } from '../../../common/types/anomaly_detection_jobs'; import { Calendar } from '../../../common/types/calendars'; @@ -40,7 +41,7 @@ declare interface JobService { ): Promise; createResultsUrl(jobId: string[], start: number, end: number, location: string): string; getJobAndGroupIds(): Promise; - searchPreview(job: CombinedJob): Promise>; + searchPreview(job: CombinedJob): Promise>; getJob(jobId: string): CombinedJob; loadJobsWrapper(): Promise; } diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index bb0cdc89904f8..d26e650d145cb 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -10,6 +10,7 @@ import { MlApiServices } from '../ml_api_service'; import type { AnomalyRecordDoc } from '../../../../common/types/anomalies'; import { InfluencersFilterQuery } from '../../../../common/types/es_client'; import { EntityField } from '../../../../common/util/anomaly_utils'; +import { RuntimeMappings } from '../../../../common/types/fields'; type RecordForInfluencer = AnomalyRecordDoc; export function resultsServiceProvider( @@ -64,6 +65,7 @@ export function resultsServiceProvider( earliestMs: number, latestMs: number, intervalMs: number, + runtimeMappings?: RuntimeMappings, indicesOptions?: IndicesOptions ): Promise; getEventDistributionData( diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index fa0bcd6ea987d..b041267f46c04 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -14,6 +14,7 @@ import { SWIM_LANE_DEFAULT_PAGE_SIZE, } from '../../explorer/explorer_constants'; import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; +import { isPopulatedObject } from '../../../../common/util/object_utils'; /** * Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards. @@ -1059,6 +1060,7 @@ export function resultsServiceProvider(mlApiServices) { earliestMs, latestMs, intervalMs, + runtimeMappings, indicesOptions ) { return new Promise((resolve, reject) => { @@ -1109,6 +1111,12 @@ export function resultsServiceProvider(mlApiServices) { }, }, }, + // Runtime mappings only needed to support when query includes a runtime field + // even though the default timeField can be a search time runtime field + // because currently Kibana doesn't support that + ...(isPopulatedObject(runtimeMappings) && query + ? { runtime_mappings: runtimeMappings } + : {}), }, ...(indicesOptions ?? {}), }) diff --git a/x-pack/plugins/ml/public/embeddables/types.ts b/x-pack/plugins/ml/public/embeddables/types.ts index 05aea1770a415..60355dae5baf4 100644 --- a/x-pack/plugins/ml/public/embeddables/types.ts +++ b/x-pack/plugins/ml/public/embeddables/types.ts @@ -81,8 +81,8 @@ export interface SwimLaneDrilldownContext extends EditSwimlanePanelContext { export function isSwimLaneEmbeddable(arg: unknown): arg is SwimLaneDrilldownContext { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('embeddable') && + isPopulatedObject(arg, ['embeddable']) && + isPopulatedObject(arg.embeddable, ['type']) && arg.embeddable.type === ANOMALY_SWIMLANE_EMBEDDABLE_TYPE ); } @@ -130,8 +130,8 @@ export function isAnomalyExplorerEmbeddable( arg: unknown ): arg is AnomalyChartsFieldSelectionContext { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('embeddable') && + isPopulatedObject(arg, ['embeddable']) && + isPopulatedObject(arg.embeddable, ['type']) && arg.embeddable.type === ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE ); } diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 9280f4603b343..56b8ca409ac0b 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -50,7 +50,7 @@ export { getSeverityType, getFormattedSeverityScore, } from '../common/util/anomaly_utils'; -export { HITS_TOTAL_RELATION } from '../common/types/es_client'; +export { ES_CLIENT_TOTAL_HITS_RELATION } from '../common/types/es_client'; export { ANOMALY_SEVERITY } from '../common'; export { useMlHref, ML_PAGES, MlUrlGenerator } from './ml_url_generator'; diff --git a/x-pack/plugins/ml/server/lib/spaces_utils.ts b/x-pack/plugins/ml/server/lib/spaces_utils.ts index 5a1d3b89ef57d..21ef163734883 100644 --- a/x-pack/plugins/ml/server/lib/spaces_utils.ts +++ b/x-pack/plugins/ml/server/lib/spaces_utils.ts @@ -5,25 +5,20 @@ * 2.0. */ -import { Legacy } from 'kibana'; import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesPluginStart } from '../../../spaces/server'; import { PLUGIN_ID } from '../../common/constants/app'; -export type RequestFacade = KibanaRequest | Legacy.Request; - export function spacesUtilsProvider( getSpacesPlugin: (() => Promise) | undefined, - request: RequestFacade + request: KibanaRequest ) { async function isMlEnabledInSpace(): Promise { if (getSpacesPlugin === undefined) { // if spaces is disabled force isMlEnabledInSpace to be true return true; } - const space = await (await getSpacesPlugin()).spacesService.getActiveSpace( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); + const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(request); return space.disabledFeatures.includes(PLUGIN_ID) === false; } @@ -31,9 +26,7 @@ export function spacesUtilsProvider( if (getSpacesPlugin === undefined) { return null; } - const client = (await getSpacesPlugin()).spacesService.createSpacesClient( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); + const client = (await getSpacesPlugin()).spacesService.createSpacesClient(request); return await client.getAll(); } @@ -58,9 +51,7 @@ export function spacesUtilsProvider( // if spaces is disabled force isMlEnabledInSpace to be true return null; } - const space = await (await getSpacesPlugin()).spacesService.getActiveSpace( - request instanceof KibanaRequest ? request : KibanaRequest.from(request) - ); + const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(request); return space.id; } diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 4c79855f39e89..3f0a02f5eaad8 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -25,7 +25,6 @@ import { isClassificationAnalysis, } from '../../../common/util/analytics_utils'; import { extractErrorMessage } from '../../../common/util/errors'; -import { SearchResponse7 } from '../../../common'; import { AnalysisConfig, DataFrameAnalyticsConfig, @@ -42,7 +41,7 @@ interface CardinalityAgg { }; } -type ValidationSearchResult = Omit & { +type ValidationSearchResult = Omit & { aggregations: MissingAgg | CardinalityAgg; }; diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index b0ee20763f430..5fecb3d9eb1ec 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -5,9 +5,10 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; + import { IScopedClusterClient } from 'kibana/server'; import { chunk } from 'lodash'; -import { SearchResponse } from 'elasticsearch'; import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/categorization_job'; import { Token, @@ -61,7 +62,7 @@ export function categorizationExamplesProvider({ } } } - const { body } = await asCurrentUser.search>({ + const { body } = await asCurrentUser.search>({ index: indexPatternTitle, size, body: { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 851336056a7f5..82d6f6ca3e103 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; + import { CategoryId, Category } from '../../../../../common/types/categories'; import type { MlClient } from '../../../../lib/ml_client'; export function topCategoriesProvider(mlClient: MlClient) { async function getTotalCategories(jobId: string): Promise { - const { body } = await mlClient.anomalySearch>( + const { body } = await mlClient.anomalySearch>( { size: 0, body: { @@ -35,12 +36,11 @@ export function topCategoriesProvider(mlClient: MlClient) { }, [] ); - // @ts-ignore total is an object here - return body?.hits?.total?.value ?? 0; + return typeof body.hits.total === 'number' ? body.hits.total : body.hits.total.value; } async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { - const { body } = await mlClient.anomalySearch>( + const { body } = await mlClient.anomalySearch>( { size: 0, body: { diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index 949159b67d33a..64dfb84be8668 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -8,13 +8,15 @@ import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; -import { HITS_TOTAL_RELATION } from '../../../common/types/es_client'; +import { ES_CLIENT_TOTAL_HITS_RELATION } from '../../../common/types/es_client'; import type { MlClient } from '../../lib/ml_client'; const callAs = { fieldCaps: () => Promise.resolve({ body: { fields: [] } }), search: () => - Promise.resolve({ body: { hits: { total: { value: 1, relation: HITS_TOTAL_RELATION.EQ } } } }), + Promise.resolve({ + body: { hits: { total: { value: 1, relation: ES_CLIENT_TOTAL_HITS_RELATION.EQ } } }, + }), }; const mlClusterClient = ({ diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index 70ffecd11c96c..1f5bcbc23423a 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { runtimeMappingsSchema } from './runtime_mappings_schema'; export const dataAnalyticsJobConfigSchema = schema.object({ description: schema.maybe(schema.string()), @@ -16,6 +17,7 @@ export const dataAnalyticsJobConfigSchema = schema.object({ source: schema.object({ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), query: schema.maybe(schema.any()), + runtime_mappings: runtimeMappingsSchema, _source: schema.maybe( schema.object({ /** Fields to include in results */ @@ -51,6 +53,7 @@ export const dataAnalyticsExplainSchema = schema.object({ source: schema.object({ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), query: schema.maybe(schema.any()), + runtime_mappings: runtimeMappingsSchema, }), analysis: schema.any(), analyzed_fields: schema.maybe(schema.any()), diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts index c3fad2f53e260..0d7e55d29b1c5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts @@ -6,27 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { isRuntimeField } from '../../../common/util/runtime_field_utils'; +import { runtimeMappingsSchema } from './runtime_mappings_schema'; export const indexPatternTitleSchema = schema.object({ /** Title of the index pattern for which to return stats. */ indexPatternTitle: schema.string(), }); -const runtimeMappingsSchema = schema.maybe( - schema.object( - {}, - { - unknowns: 'allow', - validate: (v: object) => { - if (Object.values(v).some((o) => !isRuntimeField(o))) { - return 'Invalid runtime field'; - } - }, - } - ) -); - export const dataVisualizerFieldHistogramsSchema = schema.object({ /** Query to match documents in the index. */ query: schema.any(), diff --git a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts index 76a307e710dc8..c598378926832 100644 --- a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { indicesOptionsSchema } from './datafeeds_schema'; +import { runtimeMappingsSchema } from './runtime_mappings_schema'; export const getCardinalityOfFieldsSchema = schema.object({ /** Index or indexes for which to return the time range. */ @@ -31,6 +32,6 @@ export const getTimeFieldRangeSchema = schema.object({ /** Query to match documents in the index(es). */ query: schema.maybe(schema.any()), /** Additional search options. */ - runtimeMappings: schema.maybe(schema.any()), + runtimeMappings: runtimeMappingsSchema, indicesOptions: indicesOptionsSchema, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index 8e160094c68eb..fec6439632129 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { anomalyDetectionJobSchema } from './anomaly_detectors_schema'; import { datafeedConfigSchema, indicesOptionsSchema } from './datafeeds_schema'; +import { runtimeMappingsSchema } from './runtime_mappings_schema'; export const categorizationFieldExamplesSchema = { indexPatternTitle: schema.string(), @@ -18,7 +19,7 @@ export const categorizationFieldExamplesSchema = { start: schema.number(), end: schema.number(), analyzer: schema.any(), - runtimeMappings: schema.maybe(schema.any()), + runtimeMappings: runtimeMappingsSchema, indicesOptions: indicesOptionsSchema, }; @@ -32,7 +33,7 @@ export const chartSchema = { aggFieldNamePairs: schema.arrayOf(schema.any()), splitFieldName: schema.maybe(schema.nullable(schema.string())), splitFieldValue: schema.maybe(schema.nullable(schema.string())), - runtimeMappings: schema.maybe(schema.any()), + runtimeMappings: runtimeMappingsSchema, indicesOptions: indicesOptionsSchema, }; diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index ad2bafdfb5dd1..ddb6800e13fcd 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { analysisConfigSchema, anomalyDetectionJobSchema } from './anomaly_detectors_schema'; import { datafeedConfigSchema, indicesOptionsSchema } from './datafeeds_schema'; +import { runtimeMappingsSchema } from './runtime_mappings_schema'; export const estimateBucketSpanSchema = schema.object({ aggTypes: schema.arrayOf(schema.nullable(schema.string())), @@ -18,7 +19,7 @@ export const estimateBucketSpanSchema = schema.object({ query: schema.any(), splitField: schema.maybe(schema.string()), timeField: schema.maybe(schema.string()), - runtimeMappings: schema.maybe(schema.any()), + runtimeMappings: runtimeMappingsSchema, indicesOptions: indicesOptionsSchema, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/runtime_mappings_schema.ts b/x-pack/plugins/ml/server/routes/schemas/runtime_mappings_schema.ts new file mode 100644 index 0000000000000..55247a79145c0 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/schemas/runtime_mappings_schema.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 { schema } from '@kbn/config-schema'; +import { isRuntimeField } from '../../../common/util/runtime_field_utils'; + +export const runtimeMappingsSchema = schema.maybe( + schema.object( + {}, + { + unknowns: 'allow', + validate: (v: object) => { + if (Object.values(v).some((o) => !isRuntimeField(o))) { + return 'Invalid runtime field'; + } + }, + } + ) +); diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index 1e3dcd7de5240..85cd73ba010af 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; + import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { MlLicense } from '../../../common/license'; import { CloudSetup } from '../../../../cloud/server'; import { spacesUtilsProvider } from '../../lib/spaces_utils'; @@ -23,7 +24,7 @@ export interface MlSystemProvider { ): { mlCapabilities(): Promise; mlInfo(): Promise; - mlAnomalySearch(searchParams: any, jobIds: string[]): Promise>; + mlAnomalySearch(searchParams: any, jobIds: string[]): Promise>; }; } @@ -69,7 +70,10 @@ export function getMlSystemProvider( }; }); }, - async mlAnomalySearch(searchParams: any, jobIds: string[]): Promise> { + async mlAnomalySearch( + searchParams: any, + jobIds: string[] + ): Promise> { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canAccessML']) diff --git a/x-pack/plugins/monitoring/server/deprecations.test.js b/x-pack/plugins/monitoring/server/deprecations.test.js index d7e1a2340d295..2931f704a4478 100644 --- a/x-pack/plugins/monitoring/server/deprecations.test.js +++ b/x-pack/plugins/monitoring/server/deprecations.test.js @@ -16,8 +16,8 @@ describe('monitoring plugin deprecations', function () { beforeAll(function () { const deprecations = deprecationsModule({ rename, renameFromRoot }); - transformDeprecations = (settings, fromPath, log = noop) => { - deprecations.forEach((deprecation) => deprecation(settings, fromPath, log)); + transformDeprecations = (settings, fromPath, addDeprecation = noop) => { + deprecations.forEach((deprecation) => deprecation(settings, fromPath, addDeprecation)); }; }); @@ -31,9 +31,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).not.toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).not.toHaveBeenCalled(); }); it(`shouldn't log when email_address is specified`, function () { @@ -46,9 +46,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).not.toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).not.toHaveBeenCalled(); }); it(`should log when email_address is missing, but alerts/notifications are both enabled`, function () { @@ -60,9 +60,9 @@ describe('monitoring plugin deprecations', function () { }, }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).toHaveBeenCalled(); }); }); @@ -70,65 +70,65 @@ describe('monitoring plugin deprecations', function () { it('logs a warning if elasticsearch.username is set to "elastic"', () => { const settings = { elasticsearch: { username: 'elastic' } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).toHaveBeenCalled(); }); it('logs a warning if elasticsearch.username is set to "kibana"', () => { const settings = { elasticsearch: { username: 'kibana' } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).toHaveBeenCalled(); }); it('does not log a warning if elasticsearch.username is set to something besides "elastic" or "kibana"', () => { const settings = { elasticsearch: { username: 'otheruser' } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).not.toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).not.toHaveBeenCalled(); }); it('does not log a warning if elasticsearch.username is unset', () => { const settings = { elasticsearch: { username: undefined } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).not.toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).not.toHaveBeenCalled(); }); it('logs a warning if ssl.key is set and ssl.certificate is not', () => { const settings = { elasticsearch: { ssl: { key: '' } } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).toHaveBeenCalled(); }); it('logs a warning if ssl.certificate is set and ssl.key is not', () => { const settings = { elasticsearch: { ssl: { certificate: '' } } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).toHaveBeenCalled(); }); it('does not log a warning if both ssl.key and ssl.certificate are set', () => { const settings = { elasticsearch: { ssl: { key: '', certificate: '' } } }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); - expect(log).not.toHaveBeenCalled(); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); + expect(addDeprecation).not.toHaveBeenCalled(); }); }); describe('xpack_api_polling_frequency_millis', () => { it('should call rename for this renamed config key', () => { const settings = { xpack_api_polling_frequency_millis: 30000 }; - const log = jest.fn(); - transformDeprecations(settings, fromPath, log); + const addDeprecation = jest.fn(); + transformDeprecations(settings, fromPath, addDeprecation); expect(rename).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index a276cfcee0d35..79b879b3a5f8b 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -44,41 +44,41 @@ export const deprecations = ({ 'monitoring.ui.elasticsearch.logFetchCount' ), renameFromRoot('xpack.monitoring', 'monitoring'), - (config, fromPath, logger) => { + (config, fromPath, addDeprecation) => { const emailNotificationsEnabled = get(config, 'cluster_alerts.email_notifications.enabled'); if (emailNotificationsEnabled && !get(config, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { - logger( - `Config key [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] will be required for email notifications to work in 7.0."` - ); + addDeprecation({ + message: `Config key [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] will be required for email notifications to work in 7.0."`, + }); } return config; }, - (config, fromPath, logger) => { + (config, fromPath, addDeprecation) => { const es: Record = get(config, 'elasticsearch'); if (es) { if (es.username === 'elastic') { - logger( - `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.` - ); + addDeprecation({ + message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, + }); } else if (es.username === 'kibana') { - logger( - `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.` - ); + addDeprecation({ + message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, + }); } } return config; }, - (config, fromPath, logger) => { + (config, fromPath, addDeprecation) => { const ssl: Record = get(config, 'elasticsearch.ssl'); if (ssl) { if (ssl.key !== undefined && ssl.certificate === undefined) { - logger( - `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` - ); + addDeprecation({ + message: `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + }); } else if (ssl.certificate !== undefined && ssl.key === undefined) { - logger( - `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.` - ); + addDeprecation({ + message: `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, + }); } } return config; diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 84aa1be9a8d87..5c47d0376581a 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "observability"], - "optionalPlugins": ["licensing", "home", "usageCollection"], + "optionalPlugins": ["licensing", "home", "usageCollection","lens"], "requiredPlugins": ["data"], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg b/x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg new file mode 100644 index 0000000000000..834dd98d60e4c --- /dev/null +++ b/x-pack/plugins/observability/public/assets/kibana_dashboard_dark.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg b/x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg new file mode 100644 index 0000000000000..958d25362c439 --- /dev/null +++ b/x-pack/plugins/observability/public/assets/kibana_dashboard_light.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index a41e3364d22b6..8b86e0b25379b 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -59,13 +59,13 @@ export function Header({ color, datePicker = null, restrictWidth }: Props) { - + - +

{i18n.translate('xpack.observability.home.title', { diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index e5f100be285e1..d29481a39eb72 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -56,6 +56,32 @@ describe('APMSection', () => { } as unknown) as ObservabilityPublicPluginsStart, })); }); + + it('renders transaction stat less then 1k', () => { + const resp = { + appLink: '/app/apm', + stats: { + services: { value: 11, type: 'number' }, + transactions: { value: 900, type: 'number' }, + }, + series: { + transactions: { coordinates: [] }, + }, + }; + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: resp, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const { getByText, queryAllByTestId } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('View in app')).toBeInTheDocument(); + expect(getByText('Services 11')).toBeInTheDocument(); + expect(getByText('Throughput 900.0 tpm')).toBeInTheDocument(); + expect(queryAllByTestId('loading')).toEqual([]); + }); + it('renders with transaction series and stats', () => { jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ data: response, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index 91a536840ecbd..e71468d3b028c 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -31,6 +31,19 @@ function formatTpm(value?: number) { return numeral(value).format('0.00a'); } +function formatTpmStat(value?: number) { + if (!value || value === 0) { + return '0'; + } + if (value <= 0.1) { + return '< 0.1'; + } + if (value > 1000) { + return numeral(value).format('0.00a'); + } + return numeral(value).format('0,0.0'); +} + export function APMSection({ bucketSize }: Props) { const theme = useContext(ThemeContext); const chartTheme = useChartTheme(); @@ -93,7 +106,7 @@ export function APMSection({ bucketSize }: Props) { + + + ); +} + +const Wrapper = styled.div` + text-align: center; + opacity: 0.4; + height: 550px; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx new file mode 100644 index 0000000000000..37597e0ce513f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -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 React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { mockIndexPattern, render } from '../rtl_helpers'; +import { buildFilterLabel, FilterLabel } from './filter_label'; +import * as useSeriesHook from '../hooks/use_series_filters'; + +describe('FilterLabel', function () { + const invertFilter = jest.fn(); + jest.spyOn(useSeriesHook, 'useSeriesFilters').mockReturnValue({ + invertFilter, + } as any); + + it('should render properly', async function () { + render( + + ); + + await waitFor(() => { + screen.getByText('elastic-co'); + screen.getByText(/web application:/i); + screen.getByTitle('Delete Web Application: elastic-co'); + screen.getByRole('button', { + name: /delete web application: elastic-co/i, + }); + }); + }); + + it.skip('should delete filter', async function () { + const removeFilter = jest.fn(); + render( + + ); + + await waitFor(() => { + fireEvent.click(screen.getByLabelText('Filter actions')); + }); + + fireEvent.click(screen.getByTestId('deleteFilter')); + expect(removeFilter).toHaveBeenCalledTimes(1); + expect(removeFilter).toHaveBeenCalledWith('service.name', 'elastic-co', false); + }); + + it.skip('should invert filter', async function () { + const removeFilter = jest.fn(); + render( + + ); + + await waitFor(() => { + fireEvent.click(screen.getByLabelText('Filter actions')); + }); + + fireEvent.click(screen.getByTestId('negateFilter')); + expect(invertFilter).toHaveBeenCalledTimes(1); + expect(invertFilter).toHaveBeenCalledWith({ + field: 'service.name', + negate: false, + value: 'elastic-co', + }); + }); + + it('should display invert filter', async function () { + render( + + ); + + await waitFor(() => { + screen.getByText('elastic-co'); + screen.getByText(/web application:/i); + screen.getByTitle('Delete NOT Web Application: elastic-co'); + screen.getByRole('button', { + name: /delete not web application: elastic-co/i, + }); + }); + }); + + it('should build filter meta', function () { + expect( + buildFilterLabel({ + field: 'user_agent.name', + label: 'Browser family', + indexPattern: mockIndexPattern, + value: 'Firefox', + negate: false, + }) + ).toEqual({ + meta: { + alias: null, + disabled: false, + index: 'apm-*', + key: 'Browser family', + negate: false, + type: 'phrase', + value: 'Firefox', + }, + query: { + match_phrase: { + 'user_agent.name': 'Firefox', + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx new file mode 100644 index 0000000000000..3d6dc5b3f2bf5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { injectI18n } from '@kbn/i18n/react'; +import { esFilters, Filter, IndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useSeriesFilters } from '../hooks/use_series_filters'; + +interface Props { + field: string; + label: string; + value: string; + seriesId: string; + negate: boolean; + definitionFilter?: boolean; + removeFilter: (field: string, value: string, notVal: boolean) => void; +} +export function buildFilterLabel({ + field, + value, + label, + indexPattern, + negate, +}: { + label: string; + value: string; + negate: boolean; + field: string; + indexPattern: IndexPattern; +}) { + const indexField = indexPattern.getFieldByName(field)!; + + const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); + + filter.meta.value = value; + filter.meta.key = label; + filter.meta.alias = null; + filter.meta.negate = negate; + filter.meta.disabled = false; + filter.meta.type = 'phrase'; + + return filter; +} +export function FilterLabel({ + label, + seriesId, + field, + value, + negate, + removeFilter, + definitionFilter, +}: Props) { + const FilterItem = injectI18n(esFilters.FilterItem); + + const { indexPattern } = useIndexPatternContext(); + + const filter = buildFilterLabel({ field, value, label, indexPattern, negate }); + + const { invertFilter } = useSeriesFilters({ seriesId }); + + const { + services: { uiSettings }, + } = useKibana(); + + return indexPattern ? ( + { + removeFilter(field, value, false); + }} + onUpdate={(filterN: Filter) => { + if (definitionFilter) { + // FIXME handle this use case + } else if (filterN.meta.negate !== negate) { + invertFilter({ field, value, negate }); + } + }} + uiSettings={uiSettings!} + hiddenPanelOptions={['pinFilter', 'editFilter', 'disableFilter']} + /> + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts new file mode 100644 index 0000000000000..aa3ac2fa64317 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AppDataType, ReportViewTypeId } from '../types'; +import { + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, +} from './data/elasticsearch_fieldnames'; + +export const FieldLabels: Record = { + 'user_agent.name': 'Browser family', + 'user_agent.version': 'Browser version', + 'user_agent.os.name': 'Operating system', + 'client.geo.country_name': 'Location', + 'user_agent.device.name': 'Device', + 'observer.geo.name': 'Observer location', + 'service.name': 'Service Name', + 'service.environment': 'Environment', + + [LCP_FIELD]: 'Largest contentful paint', + [FCP_FIELD]: 'First contentful paint', + [TBT_FIELD]: 'Total blocking time', + [FID_FIELD]: 'First input delay', + [CLS_FIELD]: 'Cumulative layout shift', + + 'monitor.id': 'Monitor Id', + 'monitor.status': 'Monitor Status', + + 'agent.hostname': 'Agent host', + 'host.hostname': 'Host name', + 'monitor.name': 'Monitor name', + 'monitor.type': 'Monitor Type', + 'url.port': 'Port', + tags: 'Tags', + + // custom + + 'performance.metric': 'Metric', + 'Business.KPI': 'KPI', +}; + +export const DataViewLabels: Record = { + pld: 'Performance Distribution', + upd: 'Uptime monitor duration', + upp: 'Uptime pings', + svl: 'APM Service latency', + kpi: 'KPI over time', + tpt: 'APM Service throughput', + cpu: 'System CPU Usage', + logs: 'Logs Frequency', + mem: 'System Memory Usage', + nwk: 'Network Activity', +}; + +export const ReportToDataTypeMap: Record = { + upd: 'synthetics', + upp: 'synthetics', + tpt: 'apm', + svl: 'apm', + kpi: 'rum', + pld: 'rum', + nwk: 'metrics', + mem: 'metrics', + logs: 'logs', + cpu: 'metrics', +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts new file mode 100644 index 0000000000000..5a4fb2aa3a6a5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/cpu_usage_config.ts @@ -0,0 +1,42 @@ +/* + * Copyright 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'cpu-usage', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'system.cpu.user.pct', + label: 'CPU Usage %', + }, + hasMetricType: true, + defaultFilters: [], + breakdowns: ['host.hostname'], + filters: [], + labels: { ...FieldLabels, 'host.hostname': 'Host name' }, + reportDefinitions: [ + { + field: 'agent.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts new file mode 100644 index 0000000000000..3faf54fff3140 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/elasticsearch_fieldnames.ts @@ -0,0 +1,144 @@ +/* + * Copyright 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 CLOUD = 'cloud'; +export const CLOUD_AVAILABILITY_ZONE = 'cloud.availability_zone'; +export const CLOUD_PROVIDER = 'cloud.provider'; +export const CLOUD_REGION = 'cloud.region'; +export const CLOUD_MACHINE_TYPE = 'cloud.machine.type'; + +export const SERVICE = 'service'; +export const SERVICE_NAME = 'service.name'; +export const SERVICE_ENVIRONMENT = 'service.environment'; +export const SERVICE_FRAMEWORK_NAME = 'service.framework.name'; +export const SERVICE_FRAMEWORK_VERSION = 'service.framework.version'; +export const SERVICE_LANGUAGE_NAME = 'service.language.name'; +export const SERVICE_LANGUAGE_VERSION = 'service.language.version'; +export const SERVICE_RUNTIME_NAME = 'service.runtime.name'; +export const SERVICE_RUNTIME_VERSION = 'service.runtime.version'; +export const SERVICE_NODE_NAME = 'service.node.name'; +export const SERVICE_VERSION = 'service.version'; + +export const AGENT = 'agent'; +export const AGENT_NAME = 'agent.name'; +export const AGENT_VERSION = 'agent.version'; + +export const URL_FULL = 'url.full'; +export const HTTP_REQUEST_METHOD = 'http.request.method'; +export const HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code'; +export const USER_ID = 'user.id'; +export const USER_AGENT_ORIGINAL = 'user_agent.original'; +export const USER_AGENT_NAME = 'user_agent.name'; +export const USER_AGENT_VERSION = 'user_agent.version'; + +export const DESTINATION_ADDRESS = 'destination.address'; + +export const OBSERVER_HOSTNAME = 'observer.hostname'; +export const OBSERVER_VERSION_MAJOR = 'observer.version_major'; +export const OBSERVER_LISTENING = 'observer.listening'; +export const PROCESSOR_EVENT = 'processor.event'; + +export const TRANSACTION_DURATION = 'transaction.duration.us'; +export const TRANSACTION_DURATION_HISTOGRAM = 'transaction.duration.histogram'; +export const TRANSACTION_TYPE = 'transaction.type'; +export const TRANSACTION_RESULT = 'transaction.result'; +export const TRANSACTION_NAME = 'transaction.name'; +export const TRANSACTION_ID = 'transaction.id'; +export const TRANSACTION_SAMPLED = 'transaction.sampled'; +export const TRANSACTION_BREAKDOWN_COUNT = 'transaction.breakdown.count'; +export const TRANSACTION_PAGE_URL = 'transaction.page.url'; +// for transaction metrics +export const TRANSACTION_ROOT = 'transaction.root'; + +export const EVENT_OUTCOME = 'event.outcome'; + +export const TRACE_ID = 'trace.id'; + +export const SPAN_DURATION = 'span.duration.us'; +export const SPAN_TYPE = 'span.type'; +export const SPAN_SUBTYPE = 'span.subtype'; +export const SPAN_SELF_TIME_SUM = 'span.self_time.sum.us'; +export const SPAN_ACTION = 'span.action'; +export const SPAN_NAME = 'span.name'; +export const SPAN_ID = 'span.id'; +export const SPAN_DESTINATION_SERVICE_RESOURCE = 'span.destination.service.resource'; +export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT = + 'span.destination.service.response_time.count'; + +export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM = + 'span.destination.service.response_time.sum.us'; + +// Parent ID for a transaction or span +export const PARENT_ID = 'parent.id'; + +export const ERROR_GROUP_ID = 'error.grouping_key'; +export const ERROR_CULPRIT = 'error.culprit'; +export const ERROR_LOG_LEVEL = 'error.log.level'; +export const ERROR_LOG_MESSAGE = 'error.log.message'; +export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used in es queries, since error.exception is now an array +export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array +export const ERROR_EXC_TYPE = 'error.exception.type'; +export const ERROR_PAGE_URL = 'error.page.url'; + +// METRICS +export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; +export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; +export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; +export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; +export const METRIC_CGROUP_MEMORY_LIMIT_BYTES = 'system.process.cgroup.memory.mem.limit.bytes'; +export const METRIC_CGROUP_MEMORY_USAGE_BYTES = 'system.process.cgroup.memory.mem.usage.bytes'; + +export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max'; +export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed'; +export const METRIC_JAVA_HEAP_MEMORY_USED = 'jvm.memory.heap.used'; +export const METRIC_JAVA_NON_HEAP_MEMORY_MAX = 'jvm.memory.non_heap.max'; +export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = 'jvm.memory.non_heap.committed'; +export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used'; +export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count'; +export const METRIC_JAVA_GC_COUNT = 'jvm.gc.count'; +export const METRIC_JAVA_GC_TIME = 'jvm.gc.time'; + +export const LABEL_NAME = 'labels.name'; + +export const HOST = 'host'; +export const HOST_NAME = 'host.hostname'; +export const HOST_OS_PLATFORM = 'host.os.platform'; +export const CONTAINER_ID = 'container.id'; +export const KUBERNETES = 'kubernetes'; +export const POD_NAME = 'kubernetes.pod.name'; + +export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code'; +export const CLIENT_GEO_COUNTRY_NAME = 'client.geo.country_name'; + +// RUM Labels +export const TRANSACTION_URL = 'url.full'; +export const CLIENT_GEO = 'client.geo'; +export const USER_AGENT_DEVICE = 'user_agent.device.name'; +export const USER_AGENT_OS = 'user_agent.os.name'; + +export const TRANSACTION_TIME_TO_FIRST_BYTE = 'transaction.marks.agent.timeToFirstByte'; +export const TRANSACTION_DOM_INTERACTIVE = 'transaction.marks.agent.domInteractive'; + +export const FCP_FIELD = 'transaction.marks.agent.firstContentfulPaint'; +export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint'; +export const TBT_FIELD = 'transaction.experience.tbt'; +export const FID_FIELD = 'transaction.experience.fid'; +export const CLS_FIELD = 'transaction.experience.cls'; + +export const PROFILE_ID = 'profile.id'; +export const PROFILE_DURATION = 'profile.duration'; +export const PROFILE_TOP_ID = 'profile.top.id'; +export const PROFILE_STACK = 'profile.stack'; + +export const PROFILE_SAMPLES_COUNT = 'profile.samples.count'; +export const PROFILE_CPU_NS = 'profile.cpu.ns'; +export const PROFILE_WALL_US = 'profile.wall.us'; + +export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count'; +export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes'; +export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count'; +export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts new file mode 100644 index 0000000000000..9b299e7d70bcc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/sample_attribute.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const sampleAttribute = { + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', + references: [ + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: { + layer1: { + columnOrder: ['x-axis-column', 'y-axis-column'], + columns: { + 'x-axis-column': { + sourceField: 'transaction.duration.us', + label: 'Page load time', + dataType: 'number', + operationType: 'range', + isBucketed: true, + scale: 'interval', + params: { + type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', + }, + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + fittingFunction: 'Linear', + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', + layers: [ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], + xAccessor: 'x-axis-column', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [ + { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, + { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, + ], + }, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json new file mode 100644 index 0000000000000..31fec1fe8d4f4 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/data/test_index_pattern.json @@ -0,0 +1,11 @@ +{ + "attributes": { + "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"}}", + "fields": "[{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.build.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.ephemeral_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"as.organization.name\"}}},{\"name\":\"child.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"client.as.organization.name\"}}},{\"name\":\"client.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"client.user.full_name\"}}},{\"name\":\"client.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"client.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"client.user.name\"}}},{\"name\":\"client.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.account.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.account.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.availability_zone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.image.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.instance.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.instance.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.machine.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.project.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.project.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.provider\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.region\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"cloud.service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen0size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen1size\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen2size\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clr.gc.gen3size\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.image.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.image.tag\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"container.runtime\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"destination.as.organization.name\"}}},{\"name\":\"destination.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"destination.user.full_name\"}}},{\"name\":\"destination.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"destination.user.name\"}}},{\"name\":\"destination.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.class\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.data\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.ttl\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.answers.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.header_flags\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.op_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.class\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.resolved_ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.response_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ecs.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.culprit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.handled\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"error.exception.module\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.exception.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.grouping_key\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.log.level\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.log.logger_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.log.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"error.log.param_message\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"error.stack_trace\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"error.stack_trace.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"error.stack_trace\"}}},{\"name\":\"error.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.action\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.category\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.created\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.dataset\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.duration\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.end\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.ingested\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.kind\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.module\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.outcome\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.provider\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.reason\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.risk_score\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.risk_score_norm\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.sequence\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.severity\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.start\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.timezone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.url\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.accessed\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.attributes\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.created\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.ctime\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.device\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.directory\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.drive_letter\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.extension\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.gid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.group\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.inode\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mime_type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mode\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mtime\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.owner\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"file.path\"}}},{\"name\":\"file.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"file.target_path\"}}},{\"name\":\"file.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.uid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.goroutines\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.active\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.allocated\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.frees\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.idle\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.mallocs\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.objects\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.allocations.total\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.cpu_fraction\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.next_gc_limit\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.total_count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.gc.total_pause.ns\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.obtained\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.released\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.stack\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"golang.heap.system.total\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.containerized\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.build\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.codename\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.os.full\"}}},{\"name\":\"host.os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.os.name\"}}},{\"name\":\"host.os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.uptime\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.user.full_name\"}}},{\"name\":\"host.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"host.user.name\"}}},{\"name\":\"host.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.content\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.content.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"http.request.body.content\"}}},{\"name\":\"http.request.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.method\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.mime_type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.referrer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.content\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.content.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"http.response.body.content\"}}},{\"name\":\"http.response.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.finished\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.mime_type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.status_code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"interface.alias\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"interface.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"interface.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.gc.alloc\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.gc.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.gc.time\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.committed\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.max\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.pool.committed\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.pool.max\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.pool.used\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.heap.used\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.non_heap.committed\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.non_heap.max\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.memory.non_heap.used\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"jvm.thread.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.container.image\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.container.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.deployment.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.namespace\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.node.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.node.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.pod.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.pod.uid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.replicaset.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kubernetes.statefulset.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.city\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.country_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.customer_email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.customer_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.customer_tier\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.env\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_encoded\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_failed\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_original\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.events_published\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.foo\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.git_rev\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.in_eu\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.ip\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.kibana_uuid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.lang\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.lorem\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.multi-line\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.plugin\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.productId\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.request_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.served_from_cache\",\"type\":\"conflict\",\"esTypes\":[\"boolean\",\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false,\"conflictDescriptions\":{\"boolean\":[\"apm-8.0.0-transaction-000001\"],\"keyword\":[\"apm-8.0.0-transaction-000002\"]}},{\"name\":\"labels.taskType\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.this-is-a-very-long-tag-name-without-any-spaces\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.u\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"labels.worker\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.file.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.level\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.logger\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.origin.file.line\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.origin.file.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.origin.function\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.facility.code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.facility.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.priority\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.severity.code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"log.syslog.severity.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"metricset.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"metricset.period\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.application\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.community_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.direction\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.forwarded_ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.iana_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.inner.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.inner.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.protocol\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.transport\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.eventloop.delay.avg.ms\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.eventloop.delay.ns\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.handles.active\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.arrayBuffers.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.external.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.heap.allocated.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.memory.heap.used.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nodejs.requests.active\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.interface.alias\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.interface.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.interface.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.egress.zone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.hostname\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.interface.alias\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.interface.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.interface.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ingress.zone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.listening\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.full.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"observer.os.full\"}}},{\"name\":\"observer.os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"observer.os.name\"}}},{\"name\":\"observer.os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.vendor\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"observer.version_major\",\"type\":\"number\",\"esTypes\":[\"byte\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"organization.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"organization.name\"}}},{\"name\":\"os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.full.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"os.full\"}}},{\"name\":\"os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"os.name\"}}},{\"name\":\"os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.build_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.checksum\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.install_scope\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.installed\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.license\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"parent.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.args\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.args_count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.command_line\"}}},{\"name\":\"process.entity_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.executable\"}}},{\"name\":\"process.exit_code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.name\"}}},{\"name\":\"process.parent.args\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.args_count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.exists\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.status\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.subject_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.trusted\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.valid\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.command_line\"}}},{\"name\":\"process.parent.entity_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.executable\"}}},{\"name\":\"process.parent.exit_code\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha512\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.name\"}}},{\"name\":\"process.parent.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pgid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.ppid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.start\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.thread.id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.thread.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.title\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.title.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.title\"}}},{\"name\":\"process.parent.uptime\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.parent.working_directory\"}}},{\"name\":\"process.pe.architecture\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.company\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.file_version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.imphash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.original_file_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.product\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pgid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.ppid\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.start\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.title\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.uptime\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"process.working_directory\"}}},{\"name\":\"processor.event\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"processor.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.alloc_objects.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.alloc_space.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.cpu.ns\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.duration\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.inuse_objects.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.inuse_space.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.samples.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.filename\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.function\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.stack.line\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.filename\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.function\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"profile.top.line\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.bytes\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.strings\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.hive\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.key\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.value\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.hosts\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"related.user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.gc.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.heap.allocations.total\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.heap.slots.free\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.heap.slots.live\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ruby.threads\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.author\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.category\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.license\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.ruleset\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.uuid\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"server.as.organization.name\"}}},{\"name\":\"server.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"server.user.full_name\"}}},{\"name\":\"server.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"server.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"server.user.name\"}}},{\"name\":\"server.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.environment\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.ephemeral_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.framework.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.framework.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.language.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.language.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.node.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.runtime.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.runtime.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.state\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"service.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.address\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.as.number\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.as.organization.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.as.organization.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"source.as.organization.name\"}}},{\"name\":\"source.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.country_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.region_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.mac\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.nat.ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.nat.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.packets\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"source.user.full_name\"}}},{\"name\":\"source.user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.user.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"source.user.name\"}}},{\"name\":\"source.user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sourcemap.bundle_filepath\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sourcemap.service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sourcemap.service.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.action\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.db.link\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.db.rows_affected\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.resource\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.response_time.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.response_time.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.destination.service.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.duration.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.message.age.ms\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.message.queue.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.self_time.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.self_time.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.start.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.subtype\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.sync\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"span.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.cpu.total.norm.pct\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.memory.actual.free\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.memory.total\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cgroup.memory.stats.inactive_file.bytes\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cpu.system.norm.pct\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cpu.total.norm.pct\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.cpu.user.norm.pct\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.memory.rss.bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"system.process.memory.size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tags\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.framework\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"threat.technique.name\"}}},{\"name\":\"threat.technique.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.subtechnique.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.subtechnique.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.subtechnique.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"threat.technique.subtechnique.name\"}}},{\"name\":\"threat.technique.subtechnique.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timeseries.instance\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"timestamp.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.cipher\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.certificate\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.certificate_chain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.issuer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.ja3\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.server_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.subject\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.supported_ciphers\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.client.x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.established\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.next_protocol\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.resumed\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.certificate\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.certificate_chain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.hash.md5\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.hash.sha1\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.hash.sha256\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.issuer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.ja3s\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.subject\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.server.x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"tls.version_protocol\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"trace.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.breakdown.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.histogram\",\"type\":\"histogram\",\"esTypes\":[\"histogram\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.duration.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.cls\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.fid\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.longtask.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.longtask.max\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.longtask.sum\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.experience.tbt\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.domComplete\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.domInteractive\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.firstContentfulPaint\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.largestContentfulPaint\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.agent.timeToFirstByte\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.connectEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.connectStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domComplete\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domContentLoadedEventEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domContentLoadedEventStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domInteractive\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domLoading\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domainLookupEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.domainLookupStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.fetchStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.loadEventEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.loadEventStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.requestStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.responseEnd\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.marks.navigationTiming.responseStart\",\"type\":\"number\",\"esTypes\":[\"scaled_float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.message.age.ms\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.message.queue.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"transaction.name\"}}},{\"name\":\"transaction.result\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.root\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.sampled\",\"type\":\"boolean\",\"esTypes\":[\"boolean\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.self_time.count\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.self_time.sum.us\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.span_count.dropped\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"transaction.type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.extension\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.fragment\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.original.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"url.original\"}}},{\"name\":\"url.password\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.path\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.port\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.query\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.registered_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.scheme\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.subdomain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.top_level_domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url.username\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.full_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.full_name.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"user.full_name\"}}},{\"name\":\"user.group.domain\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.hash\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.roles\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.device.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.original\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.original.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"user_agent.original\"}}},{\"name\":\"user_agent.os.family\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.full\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.kernel\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.platform\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.os.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user_agent.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vlan.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vlan.name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.category\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.classification\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.description\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.description.text\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false,\"subType\":{\"multi\":{\"parent\":\"vulnerability.description\"}}},{\"name\":\"vulnerability.enumeration\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.reference\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.report_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.scanner.vendor\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.base\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.environmental\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.temporal\",\"type\":\"number\",\"esTypes\":[\"float\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.score.version\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"vulnerability.severity\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.alternative_names\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.issuer.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.not_after\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.not_before\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_curve\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_exponent\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.public_key_size\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.serial_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.signature_algorithm\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.common_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.country\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.distinguished_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.locality\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.organization\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.organizational_unit\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.subject.state_or_province\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"x509.version_number\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", + "timeFieldName": "@timestamp" + }, + "id": "apm-*", + "type": "index-pattern", + "version": "1" +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts new file mode 100644 index 0000000000000..85d48ef638d44 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReportViewTypes } from '../types'; +import { getPerformanceDistLensConfig } from './performance_dist_config'; +import { getMonitorDurationConfig } from './monitor_duration_config'; +import { getServiceLatencyLensConfig } from './service_latency_config'; +import { getMonitorPingsConfig } from './monitor_pings_config'; +import { getServiceThroughputLensConfig } from './service_throughput_config'; +import { getKPITrendsLensConfig } from './kpi_trends_config'; +import { getCPUUsageLensConfig } from './cpu_usage_config'; +import { getMemoryUsageLensConfig } from './memory_usage_config'; +import { getNetworkActivityLensConfig } from './network_activity_config'; +import { getLogsFrequencyLensConfig } from './logs_frequency_config'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; + +interface Props { + reportType: keyof typeof ReportViewTypes; + seriesId: string; + indexPattern: IIndexPattern; +} + +export const getDefaultConfigs = ({ reportType, seriesId, indexPattern }: Props) => { + switch (ReportViewTypes[reportType]) { + case 'page-load-dist': + return getPerformanceDistLensConfig({ seriesId, indexPattern }); + case 'kpi-trends': + return getKPITrendsLensConfig({ seriesId, indexPattern }); + case 'uptime-duration': + return getMonitorDurationConfig({ seriesId }); + case 'uptime-pings': + return getMonitorPingsConfig({ seriesId }); + case 'service-latency': + return getServiceLatencyLensConfig({ seriesId, indexPattern }); + case 'service-throughput': + return getServiceThroughputLensConfig({ seriesId, indexPattern }); + case 'cpu-usage': + return getCPUUsageLensConfig({ seriesId }); + case 'memory-usage': + return getMemoryUsageLensConfig({ seriesId }); + case 'network-activity': + return getNetworkActivityLensConfig({ seriesId }); + case 'logs-frequency': + return getLogsFrequencyLensConfig({ seriesId }); + default: + return getKPITrendsLensConfig({ seriesId, indexPattern }); + } +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts new file mode 100644 index 0000000000000..a967a8824bca7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/kpi_trends_config.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { + CLIENT_GEO_COUNTRY_NAME, + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, + USER_AGENT_VERSION, +} from './data/elasticsearch_fieldnames'; + +export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId, + defaultSeriesType: 'bar_stacked', + reportType: 'kpi-trends', + seriesTypes: ['bar', 'bar_stacked'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'count', + label: 'Page views', + }, + hasMetricType: false, + defaultFilters: [ + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + { + field: USER_AGENT_NAME, + nested: USER_AGENT_VERSION, + }, + ], + breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + filters: [ + buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ], + labels: { ...FieldLabels, SERVICE_NAME: 'Web Application' }, + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + }, + { + field: 'Business.KPI', + custom: true, + defaultValue: 'Records', + options: [ + { + field: 'Records', + label: 'Page views', + }, + ], + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts new file mode 100644 index 0000000000000..dcfaed938cc0f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -0,0 +1,387 @@ +/* + * Copyright 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 { LensAttributes } from './lens_attributes'; +import { mockIndexPattern } from '../rtl_helpers'; +import { getDefaultConfigs } from './default_configs'; +import { sampleAttribute } from './data/sample_attribute'; +import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames'; +import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames'; + +describe('Lens Attribute', () => { + const reportViewConfig = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: 'series-id', + }); + + let lnsAttr: LensAttributes; + + beforeEach(() => { + lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {}); + }); + + it('should return expected json', function () { + expect(lnsAttr.getJSON()).toEqual(sampleAttribute); + }); + + it('should return main y axis', function () { + expect(lnsAttr.getMainYAxis()).toEqual({ + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }); + }); + + it('should return expected field type', function () { + expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( + JSON.stringify({ + count: 0, + name: 'transaction.type', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }) + ); + }); + + it('should return expected field type for custom field with default value', function () { + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + JSON.stringify({ + count: 0, + name: 'transaction.duration.us', + type: 'number', + esTypes: ['long'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }) + ); + }); + + it('should return expected field type for custom field with passed value', function () { + lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', { + 'performance.metric': LCP_FIELD, + }); + + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + JSON.stringify({ + count: 0, + name: LCP_FIELD, + type: 'number', + esTypes: ['scaled_float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }) + ); + }); + + it('should return expected number column', function () { + expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({ + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }); + }); + + it('should return expected date histogram column', function () { + expect(lnsAttr.getDateHistogramColumn('@timestamp')).toEqual({ + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { + interval: 'auto', + }, + scale: 'interval', + sourceField: '@timestamp', + }); + }); + + it('should return main x axis', function () { + expect(lnsAttr.getXAxis()).toEqual({ + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }); + }); + + it('should return first layer', function () { + expect(lnsAttr.getLayer()).toEqual({ + columnOrder: ['x-axis-column', 'y-axis-column'], + columns: { + 'x-axis-column': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }); + }); + + it('should return expected XYState', function () { + expect(lnsAttr.getXyState()).toEqual({ + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + curveType: 'CURVE_MONOTONE_X', + fittingFunction: 'Linear', + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + layers: [ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + palette: undefined, + seriesType: 'line', + xAccessor: 'x-axis-column', + yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + }, + ], + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + valueLabels: 'hide', + }); + }); + + describe('ParseFilters function', function () { + it('should parse default filters', function () { + expect(lnsAttr.parseFilters()).toEqual([ + { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, + { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, + ]); + }); + + it('should parse default and ui filters', function () { + lnsAttr = new LensAttributes( + mockIndexPattern, + reportViewConfig, + 'line', + [ + { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] }, + { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] }, + ], + 'count', + {} + ); + + expect(lnsAttr.parseFilters()).toEqual([ + { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, + { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, + { + meta: { + index: 'apm-*', + key: 'service.name', + params: ['elastic-co', 'kibana-front'], + type: 'phrases', + value: 'elastic-co, kibana-front', + }, + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'service.name': 'elastic-co', + }, + }, + { + match_phrase: { + 'service.name': 'kibana-front', + }, + }, + ], + }, + }, + }, + { + meta: { + index: 'apm-*', + }, + query: { + match_phrase: { + 'user_agent.name': 'Firefox', + }, + }, + }, + { + meta: { + index: 'apm-*', + negate: true, + }, + query: { + match_phrase: { + 'user_agent.name': 'Chrome', + }, + }, + }, + ]); + }); + }); + + describe('Layer breakdowns', function () { + it('should add breakdown column', function () { + lnsAttr.addBreakdown(USER_AGENT_NAME); + + expect(lnsAttr.visualization.layers).toEqual([ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + palette: undefined, + seriesType: 'line', + splitAccessor: 'break-down-column', + xAccessor: 'x-axis-column', + yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + }, + ]); + + expect(lnsAttr.layers.layer1).toEqual({ + columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'], + columns: { + 'break-down-column': { + dataType: 'string', + isBucketed: true, + label: 'Top values of Browser family', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { columnId: 'y-axis-column', type: 'column' }, + orderDirection: 'desc', + otherBucket: true, + size: 3, + }, + scale: 'ordinal', + sourceField: 'user_agent.name', + }, + 'x-axis-column': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [{ from: 0, label: '', to: 1000 }], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }, + incompleteColumns: {}, + }); + }); + + it('should remove breakdown column', function () { + lnsAttr.addBreakdown(USER_AGENT_NAME); + + lnsAttr.removeBreakdown(); + + expect(lnsAttr.visualization.layers).toEqual([ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + palette: undefined, + seriesType: 'line', + xAccessor: 'x-axis-column', + yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + }, + ]); + + expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']); + + expect(lnsAttr.layers.layer1.columns).toEqual({ + 'x-axis-column': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [{ from: 0, label: '', to: 1000 }], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts new file mode 100644 index 0000000000000..589a93d160068 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CountIndexPatternColumn, + DateHistogramIndexPatternColumn, + LastValueIndexPatternColumn, + OperationType, + PersistedIndexPatternLayer, + RangeIndexPatternColumn, + SeriesType, + TypedLensByValueInput, + XYState, + XYCurveType, + DataType, +} from '../../../../../../lens/public'; +import { + buildPhraseFilter, + buildPhrasesFilter, + IndexPattern, +} from '../../../../../../../../src/plugins/data/common'; +import { FieldLabels } from './constants'; +import { DataSeries, UrlFilter } from '../types'; + +function getLayerReferenceName(layerId: string) { + return `indexpattern-datasource-layer-${layerId}`; +} + +export class LensAttributes { + indexPattern: IndexPattern; + layers: Record; + visualization: XYState; + filters: UrlFilter[]; + seriesType: SeriesType; + reportViewConfig: DataSeries; + reportDefinitions: Record; + + constructor( + indexPattern: IndexPattern, + reportViewConfig: DataSeries, + seriesType?: SeriesType, + filters?: UrlFilter[], + metricType?: OperationType, + reportDefinitions?: Record + ) { + this.indexPattern = indexPattern; + this.layers = {}; + this.filters = filters ?? []; + this.reportDefinitions = reportDefinitions ?? {}; + + if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && metricType) { + reportViewConfig.yAxisColumn.operationType = metricType; + } + this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; + this.reportViewConfig = reportViewConfig; + this.layers.layer1 = this.getLayer(); + this.visualization = this.getXyState(); + } + + addBreakdown(sourceField: string) { + const fieldMeta = this.indexPattern.getFieldByName(sourceField); + + this.layers.layer1.columns['break-down-column'] = { + sourceField, + label: `Top values of ${FieldLabels[sourceField]}`, + dataType: fieldMeta?.type as DataType, + operationType: 'terms', + scale: 'ordinal', + isBucketed: true, + params: { + size: 3, + orderBy: { type: 'column', columnId: 'y-axis-column' }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + }, + }; + + this.layers.layer1.columnOrder = ['x-axis-column', 'break-down-column', 'y-axis-column']; + + this.visualization.layers[0].splitAccessor = 'break-down-column'; + } + + removeBreakdown() { + delete this.layers.layer1.columns['break-down-column']; + + this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column']; + + this.visualization.layers[0].splitAccessor = undefined; + } + + getNumberColumn(sourceField: string): RangeIndexPatternColumn { + return { + sourceField, + label: this.reportViewConfig.labels[sourceField], + dataType: 'number', + operationType: 'range', + isBucketed: true, + scale: 'interval', + params: { + type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', + }, + }; + } + + getDateHistogramColumn(sourceField: string): DateHistogramIndexPatternColumn { + return { + sourceField, + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { interval: 'auto' }, + scale: 'interval', + }; + } + + getXAxis(): + | LastValueIndexPatternColumn + | DateHistogramIndexPatternColumn + | RangeIndexPatternColumn { + const { xAxisColumn } = this.reportViewConfig; + + const { type: fieldType, name: fieldName } = this.getFieldMeta(xAxisColumn.sourceField)!; + + if (fieldType === 'date') { + return this.getDateHistogramColumn(fieldName); + } + if (fieldType === 'number') { + return this.getNumberColumn(fieldName); + } + + // FIXME review my approach again + return this.getDateHistogramColumn(fieldName); + } + + getFieldMeta(sourceField?: string) { + let xAxisField = sourceField; + + if (xAxisField) { + const rdf = this.reportViewConfig.reportDefinitions ?? []; + + const customField = rdf.find(({ field }) => field === xAxisField); + + if (customField) { + if (this.reportDefinitions[xAxisField]) { + xAxisField = this.reportDefinitions[xAxisField]; + } else if (customField.defaultValue) { + xAxisField = customField.defaultValue; + } else if (customField.options?.[0].field) { + xAxisField = customField.options?.[0].field; + } + } + + return this.indexPattern.getFieldByName(xAxisField); + } + } + + getMainYAxis() { + return { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + ...this.reportViewConfig.yAxisColumn, + } as CountIndexPatternColumn; + } + + getLayer() { + return { + columnOrder: ['x-axis-column', 'y-axis-column'], + columns: { + 'x-axis-column': this.getXAxis(), + 'y-axis-column': this.getMainYAxis(), + }, + incompleteColumns: {}, + }; + } + + getXyState(): XYState { + return { + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + fittingFunction: 'Linear', + curveType: 'CURVE_MONOTONE_X' as XYCurveType, + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', + layers: [ + { + accessors: ['y-axis-column'], + layerId: 'layer1', + seriesType: this.seriesType ?? 'line', + palette: this.reportViewConfig.palette, + yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], + xAccessor: 'x-axis-column', + }, + ], + }; + } + + parseFilters() { + const defaultFilters = this.reportViewConfig.filters ?? []; + const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : []; + + this.filters.forEach(({ field, values = [], notValues = [] }) => { + const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!; + + if (values?.length > 0) { + if (values?.length > 1) { + const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern); + parsedFilters.push(multiFilter); + } else { + const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern); + parsedFilters.push(filter); + } + } + + if (notValues?.length > 0) { + if (notValues?.length > 1) { + const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern); + multiFilter.meta.negate = true; + parsedFilters.push(multiFilter); + } else { + const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern); + filter.meta.negate = true; + parsedFilters.push(filter); + } + } + }); + + return parsedFilters; + } + + getJSON(): TypedLensByValueInput['attributes'] { + return { + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', + references: [ + { + id: this.indexPattern.id!, + name: 'indexpattern-datasource-current-indexpattern', + type: 'index-pattern', + }, + { + id: this.indexPattern.id!, + name: getLayerReferenceName('layer1'), + type: 'index-pattern', + }, + ], + state: { + datasourceStates: { + indexpattern: { + layers: this.layers, + }, + }, + visualization: this.visualization, + query: { query: '', language: 'kuery' }, + filters: this.parseFilters(), + }, + }; + } +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts new file mode 100644 index 0000000000000..68e5e697d2f9d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/logs_frequency_config.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataSeries } from '../types'; +import { FieldLabels } from './constants'; + +interface Props { + seriesId: string; +} + +export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'logs-frequency', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'count', + }, + hasMetricType: false, + defaultFilters: [], + breakdowns: ['agent.hostname'], + filters: [], + labels: { ...FieldLabels }, + reportDefinitions: [ + { + field: 'agent.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts new file mode 100644 index 0000000000000..579372ed86fa7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/memory_usage_config.ts @@ -0,0 +1,42 @@ +/* + * Copyright 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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'memory-usage', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'system.memory.used.pct', + label: 'Memory Usage %', + }, + hasMetricType: true, + defaultFilters: [], + breakdowns: ['host.hostname'], + filters: [], + labels: { ...FieldLabels, 'host.hostname': 'Host name' }, + reportDefinitions: [ + { + field: 'host.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.ts new file mode 100644 index 0000000000000..aa9b8b94c6d86 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_duration_config.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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getMonitorDurationConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'uptime-duration', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar_stacked'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'monitor.duration.us', + label: 'Monitor duration (ms)', + }, + hasMetricType: true, + defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'], + breakdowns: [ + 'observer.geo.name', + 'monitor.name', + 'monitor.id', + 'monitor.type', + 'tags', + 'url.port', + ], + filters: [], + reportDefinitions: [ + { + field: 'monitor.id', + }, + ], + labels: { ...FieldLabels }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.ts new file mode 100644 index 0000000000000..72968626e934b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/monitor_pings_config.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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; + +interface Props { + seriesId: string; +} + +export function getMonitorPingsConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'uptime-pings', + defaultSeriesType: 'bar_stacked', + seriesTypes: ['bar_stacked', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'count', + label: 'Monitor pings', + }, + hasMetricType: false, + defaultFilters: ['observer.geo.name'], + breakdowns: ['monitor.status', 'observer.geo.name', 'monitor.type'], + filters: [], + palette: { type: 'palette', name: 'status' }, + reportDefinitions: [ + { + field: 'monitor.id', + }, + { + field: 'url.full', + }, + ], + labels: { ...FieldLabels }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.ts new file mode 100644 index 0000000000000..63cdd0ec8bd60 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/network_activity_config.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 { DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { OperationType } from '../../../../../../lens/public'; + +interface Props { + seriesId: string; +} + +export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries { + return { + id: seriesId, + reportType: 'network-activity', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'system.memory.used.pct', + }, + hasMetricType: true, + defaultFilters: [], + breakdowns: ['host.hostname'], + filters: [], + labels: { ...FieldLabels, 'host.hostname': 'Host name' }, + reportDefinitions: [ + { + field: 'host.hostname', + required: true, + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts new file mode 100644 index 0000000000000..41617304c9f3d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/performance_dist_config.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { + CLIENT_GEO_COUNTRY_NAME, + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TBT_FIELD, + TRANSACTION_DURATION, + TRANSACTION_TYPE, + USER_AGENT_DEVICE, + USER_AGENT_NAME, + USER_AGENT_OS, + USER_AGENT_VERSION, +} from './data/elasticsearch_fieldnames'; + +export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId ?? 'unique-key', + reportType: 'page-load-dist', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: 'performance.metric', + }, + yAxisColumn: { + operationType: 'count', + label: 'Pages loaded', + }, + hasMetricType: false, + defaultFilters: [ + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + { + field: USER_AGENT_NAME, + nested: USER_AGENT_VERSION, + }, + ], + breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], + reportDefinitions: [ + { + field: SERVICE_NAME, + required: true, + }, + { + field: SERVICE_ENVIRONMENT, + }, + { + field: 'performance.metric', + custom: true, + defaultValue: TRANSACTION_DURATION, + options: [ + { label: 'Page load time', field: TRANSACTION_DURATION }, + { label: 'First contentful paint', field: FCP_FIELD }, + { label: 'Total blocking time', field: TBT_FIELD }, + // FIXME, review if we need these descriptions + { label: 'Largest contentful paint', field: LCP_FIELD, description: 'Core web vital' }, + { label: 'First input delay', field: FID_FIELD, description: 'Core web vital' }, + { label: 'Cumulative layout shift', field: CLS_FIELD, description: 'Core web vital' }, + ], + }, + ], + filters: [ + buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), + buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), + ], + labels: { + ...FieldLabels, + [SERVICE_NAME]: 'Web Application', + [TRANSACTION_DURATION]: 'Page load time', + }, + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts new file mode 100644 index 0000000000000..a31679c61a4ab --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { OperationType } from '../../../../../../lens/public'; + +export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries { + return { + id: seriesId, + reportType: 'service-latency', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'transaction.duration.us', + label: 'Latency', + }, + hasMetricType: true, + defaultFilters: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + breakdowns: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)], + labels: { ...FieldLabels }, + reportDefinitions: [ + { + field: 'service.name', + required: true, + }, + { + field: 'service.environment', + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts new file mode 100644 index 0000000000000..32cae2167ddf0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_throughput_config.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConfigProps, DataSeries } from '../types'; +import { FieldLabels } from './constants'; +import { buildPhraseFilter } from './utils'; +import { OperationType } from '../../../../../../lens/public'; + +export function getServiceThroughputLensConfig({ + seriesId, + indexPattern, +}: ConfigProps): DataSeries { + return { + id: seriesId, + reportType: 'service-latency', + defaultSeriesType: 'line', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg' as OperationType, + sourceField: 'transaction.duration.us', + label: 'Throughput', + }, + hasMetricType: true, + defaultFilters: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + breakdowns: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)], + labels: { ...FieldLabels }, + reportDefinitions: [ + { + field: 'service.name', + required: true, + }, + { + field: 'service.environment', + }, + ], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts new file mode 100644 index 0000000000000..5b99c19dbabb7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/url_constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum URL_KEYS { + METRIC_TYPE = 'mt', + REPORT_TYPE = 'rt', + SERIES_TYPE = 'st', + BREAK_DOWN = 'bd', + FILTERS = 'ft', + REPORT_DEFINITIONS = 'rdf', +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts new file mode 100644 index 0000000000000..38b8ce81b2acd --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import rison, { RisonValue } from 'rison-node'; +import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage'; +import type { SeriesUrl } from '../types'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { URL_KEYS } from './url_constants'; + +export function convertToShortUrl(series: SeriesUrl) { + const { + metric, + seriesType, + reportType, + breakdown, + filters, + reportDefinitions, + ...restSeries + } = series; + + return { + [URL_KEYS.METRIC_TYPE]: metric, + [URL_KEYS.REPORT_TYPE]: reportType, + [URL_KEYS.SERIES_TYPE]: seriesType, + [URL_KEYS.BREAK_DOWN]: breakdown, + [URL_KEYS.FILTERS]: filters, + [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, + ...restSeries, + }; +} + +export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { + const allSeriesIds = Object.keys(allSeries); + + const allShortSeries: AllShortSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); + }); + + return ( + baseHref + + `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` + ); +} + +export function buildPhraseFilter(field: string, value: any, indexPattern: IIndexPattern) { + const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field)!; + return esFilters.buildPhraseFilter(fieldMeta, value, indexPattern); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx new file mode 100644 index 0000000000000..7e99874f557b3 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { within } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/dom'; +import { render, mockUrlStorage, mockCore } from './rtl_helpers'; +import { ExploratoryView } from './exploratory_view'; +import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; +import * as obsvInd from '../../../utils/observability_index_patterns'; + +describe('ExploratoryView', () => { + beforeEach(() => { + const indexPattern = getStubIndexPattern( + 'apm-*', + () => {}, + '@timestamp', + [ + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }, + ], + mockCore() as any + ); + + jest.spyOn(obsvInd, 'ObservabilityIndexPatterns').mockReturnValue({ + getIndexPattern: jest.fn().mockReturnValue(indexPattern), + } as any); + }); + + it('renders exploratory view', async () => { + render(); + + await waitFor(() => { + screen.getByText(/open in lens/i); + screen.getByRole('heading', { name: /exploratory view/i }); + screen.getByRole('img', { name: /visulization/i }); + screen.getByText(/add series/i); + screen.getByText(/no series found, please add a series\./i); + }); + }); + + it('can add, cancel new series', async () => { + render(); + + await fireEvent.click(screen.getByText(/add series/i)); + + await waitFor(() => { + screen.getByText(/open in lens/i); + screen.getByText(/select a data type to start building a series\./i); + screen.getByRole('table', { name: /this table contains 1 rows\./i }); + const button = screen.getByRole('button', { name: /add/i }); + within(button).getByText(/add/i); + }); + + await fireEvent.click(screen.getByText(/cancel/i)); + + await waitFor(() => { + screen.getByText(/add series/i); + }); + }); + + it('renders lens component when there is series', async () => { + mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + await waitFor(() => { + screen.getByText(/open in lens/i); + screen.getByRole('heading', { name: /uptime pings/i }); + screen.getByText(/uptime-pings-histogram/i); + screen.getByText(/Lens Embeddable Component/i); + screen.getByRole('table', { name: /this table contains 1 rows\./i }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx new file mode 100644 index 0000000000000..b3ad107bbe0e2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 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 React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { EuiLoadingSpinner, EuiPanel, EuiTitle } from '@elastic/eui'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../plugin'; +import { ExploratoryViewHeader } from './header/header'; +import { SeriesEditor } from './series_editor/series_editor'; +import { useUrlStorage } from './hooks/use_url_strorage'; +import { useLensAttributes } from './hooks/use_lens_attributes'; +import { EmptyView } from './components/empty_view'; +import { useIndexPatternContext } from './hooks/use_default_index_pattern'; +import { TypedLensByValueInput } from '../../../../../lens/public'; + +export function ExploratoryView() { + const { + services: { lens }, + } = useKibana(); + + const [lensAttributes, setLensAttributes] = useState( + null + ); + + const { indexPattern } = useIndexPatternContext(); + + const LensComponent = lens?.EmbeddableComponent; + + const { firstSeriesId: seriesId, firstSeries: series } = useUrlStorage(); + + const lensAttributesT = useLensAttributes({ + seriesId, + indexPattern, + }); + + useEffect(() => { + setLensAttributes(lensAttributesT); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]); + + return ( + + {lens ? ( + <> + + {!indexPattern && ( + + + + )} + {lensAttributes && seriesId && series?.reportType && series?.time ? ( + + ) : ( + + )} + + + ) : ( + +

+ {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { + defaultMessage: + 'Lens app is not available, please enable Lens to use exploratory view.', + })} +

+
+ )} +
+ ); +} + +const SpinnerWrap = styled.div` + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx new file mode 100644 index 0000000000000..de6912f256be7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mockUrlStorage, render } from '../rtl_helpers'; +import { ExploratoryViewHeader } from './header'; +import { fireEvent } from '@testing-library/dom'; + +describe('ExploratoryViewHeader', function () { + it('should render properly', function () { + const { getByText } = render( + + ); + getByText('Open in Lens'); + }); + + it('should be able to click open in lens', function () { + mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + const { getByText, core } = render( + + ); + fireEvent.click(getByText('Open in Lens')); + + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith({ + attributes: { title: 'Performance distribution' }, + id: '', + timeRange: { + from: 'now-15m', + to: 'now', + }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx new file mode 100644 index 0000000000000..bda3566c76602 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { DataViewLabels } from '../configurations/constants'; +import { useUrlStorage } from '../hooks/use_url_strorage'; + +interface Props { + seriesId: string; + lensAttributes: TypedLensByValueInput['attributes'] | null; +} + +export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { + const { + services: { lens }, + } = useKibana(); + + const { series } = useUrlStorage(seriesId); + + return ( + + + +

+ {DataViewLabels[series.reportType] ?? + i18n.translate('xpack.observability.expView.heading.label', { + defaultMessage: 'Exploratory view', + })} +

+
+
+ + { + if (lensAttributes) { + lens.navigateToPrefilledEditor({ + id: '', + timeRange: series.time, + attributes: lensAttributes, + }); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + + +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx new file mode 100644 index 0000000000000..04cbb4a4ddb18 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_default_index_pattern.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, Context, useState, useEffect } from 'react'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { AppDataType } from '../types'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns'; + +export interface IIndexPatternContext { + indexPattern: IndexPattern; + loadIndexPattern: (dataType: AppDataType) => void; +} + +export const IndexPatternContext = createContext>({}); + +interface ProviderProps { + indexPattern?: IndexPattern; + children: JSX.Element; +} + +export function IndexPatternContextProvider({ + children, + indexPattern: initialIndexPattern, +}: ProviderProps) { + const [indexPattern, setIndexPattern] = useState(initialIndexPattern); + + useEffect(() => { + setIndexPattern(initialIndexPattern); + }, [initialIndexPattern]); + + const { + services: { data }, + } = useKibana(); + + const loadIndexPattern = async (dataType: AppDataType) => { + const obsvIndexP = new ObservabilityIndexPatterns(data); + const indPattern = await obsvIndexP.getIndexPattern(dataType); + setIndexPattern(indPattern!); + }; + + return ( + + {children} + + ); +} + +export const useIndexPatternContext = () => { + return useContext((IndexPatternContext as unknown) as Context); +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts new file mode 100644 index 0000000000000..9f462790e8d37 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_init_exploratory_view.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useFetcher } from '../../../..'; +import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { AllShortSeries } from './use_url_strorage'; +import { ReportToDataTypeMap } from '../configurations/constants'; +import { + DataType, + ObservabilityIndexPatterns, +} from '../../../../utils/observability_index_patterns'; + +export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => { + const { + services: { data }, + } = useKibana(); + + const allSeriesKey = 'sr'; + + const allSeries = storage.get(allSeriesKey) ?? {}; + + const allSeriesIds = Object.keys(allSeries); + + const firstSeriesId = allSeriesIds?.[0]; + + const firstSeries = allSeries[firstSeriesId]; + + const { data: indexPattern } = useFetcher(() => { + const obsvIndexP = new ObservabilityIndexPatterns(data); + let reportType: DataType = 'apm'; + if (firstSeries?.rt) { + reportType = ReportToDataTypeMap[firstSeries?.rt]; + } + + return obsvIndexP.getIndexPattern(reportType); + }, [firstSeries?.rt, data]); + + return indexPattern; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts new file mode 100644 index 0000000000000..1c735009f66f9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { LensAttributes } from '../configurations/lens_attributes'; +import { useUrlStorage } from './use_url_strorage'; +import { getDefaultConfigs } from '../configurations/default_configs'; + +import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { DataSeries, SeriesUrl, UrlFilter } from '../types'; + +interface Props { + seriesId: string; + indexPattern?: IndexPattern | null; +} + +export const getFiltersFromDefs = ( + reportDefinitions: SeriesUrl['reportDefinitions'], + dataViewConfig: DataSeries +) => { + const rdfFilters = Object.entries(reportDefinitions ?? {}).map(([field, value]) => { + return { + field, + values: [value], + }; + }) as UrlFilter[]; + + // let's filter out custom fields + return rdfFilters.filter(({ field }) => { + const rdf = dataViewConfig.reportDefinitions.find(({ field: fd }) => field === fd); + return !rdf?.custom; + }); +}; + +export const useLensAttributes = ({ + seriesId, + indexPattern, +}: Props): TypedLensByValueInput['attributes'] | null => { + const { series } = useUrlStorage(seriesId); + + const { breakdown, seriesType, metric: metricType, reportType, reportDefinitions = {} } = + series ?? {}; + + return useMemo(() => { + if (!indexPattern || !reportType) { + return null; + } + + const dataViewConfig = getDefaultConfigs({ + seriesId, + reportType, + indexPattern, + }); + + const filters: UrlFilter[] = (series.filters ?? []).concat( + getFiltersFromDefs(reportDefinitions, dataViewConfig) + ); + + const lensAttributes = new LensAttributes( + indexPattern, + dataViewConfig, + seriesType, + filters, + metricType, + reportDefinitions + ); + + if (breakdown) { + lensAttributes.addBreakdown(breakdown); + } + + return lensAttributes.getJSON(); + }, [ + indexPattern, + breakdown, + seriesType, + metricType, + reportType, + reportDefinitions, + seriesId, + series.filters, + ]); +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts new file mode 100644 index 0000000000000..35247180c2ee5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -0,0 +1,100 @@ +/* + * Copyright 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 { useUrlStorage } from './use_url_strorage'; +import { UrlFilter } from '../types'; + +export interface UpdateFilter { + field: string; + value: string; + negate?: boolean; +} + +export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { + const { series, setSeries } = useUrlStorage(seriesId); + + const filters = series.filters ?? []; + + const removeFilter = ({ field, value, negate }: UpdateFilter) => { + const filtersN = filters.map((filter) => { + if (filter.field === field) { + if (negate) { + const notValuesN = filter.notValues?.filter((val) => val !== value); + return { ...filter, notValues: notValuesN }; + } else { + const valuesN = filter.values?.filter((val) => val !== value); + return { ...filter, values: valuesN }; + } + } + + return filter; + }); + setSeries(seriesId, { ...series, filters: filtersN }); + }; + + const addFilter = ({ field, value, negate }: UpdateFilter) => { + const currFilter: UrlFilter = { field }; + if (negate) { + currFilter.notValues = [value]; + } else { + currFilter.values = [value]; + } + if (filters.length === 0) { + setSeries(seriesId, { ...series, filters: [currFilter] }); + } else { + setSeries(seriesId, { + ...series, + filters: [currFilter, ...filters.filter((ft) => ft.field !== field)], + }); + } + }; + + const updateFilter = ({ field, value, negate }: UpdateFilter) => { + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd) ?? { + field, + }; + + const currNotValues = currFilter.notValues ?? []; + const currValues = currFilter.values ?? []; + + const notValues = currNotValues.filter((val) => val !== value); + const values = currValues.filter((val) => val !== value); + + if (negate) { + notValues.push(value); + } else { + values.push(value); + } + + currFilter.notValues = notValues.length > 0 ? notValues : undefined; + currFilter.values = values.length > 0 ? values : undefined; + + const otherFilters = filters.filter(({ field: fd }) => fd !== field); + + if (notValues.length > 0 || values.length > 0) { + setSeries(seriesId, { ...series, filters: [...otherFilters, currFilter] }); + } else { + setSeries(seriesId, { ...series, filters: otherFilters }); + } + }; + + const setFilter = ({ field, value, negate }: UpdateFilter) => { + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); + + if (!currFilter) { + addFilter({ field, value, negate }); + } else { + updateFilter({ field, value, negate }); + } + }; + + const invertFilter = ({ field, value, negate }: UpdateFilter) => { + updateFilter({ field, value, negate: !negate }); + }; + + return { invertFilter, setFilter, removeFilter }; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx new file mode 100644 index 0000000000000..d38429703b709 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 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, { createContext, useContext, Context } from 'react'; +import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types'; +import { convertToShortUrl } from '../configurations/utils'; +import { OperationType, SeriesType } from '../../../../../../lens/public'; +import { URL_KEYS } from '../configurations/url_constants'; + +export const UrlStorageContext = createContext(null); + +interface ProviderProps { + storage: IKbnUrlStateStorage; +} + +export function UrlStorageContextProvider({ + children, + storage, +}: ProviderProps & { children: JSX.Element }) { + return {children}; +} + +function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { + const { mt, st, rt, bd, ft, time, rdf, ...restSeries } = newValue; + return { + metric: mt, + reportType: rt!, + seriesType: st, + breakdown: bd, + filters: ft!, + time: time!, + reportDefinitions: rdf, + ...restSeries, + }; +} + +interface ShortUrlSeries { + [URL_KEYS.METRIC_TYPE]?: OperationType; + [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; + [URL_KEYS.SERIES_TYPE]?: SeriesType; + [URL_KEYS.BREAK_DOWN]?: string; + [URL_KEYS.FILTERS]?: UrlFilter[]; + [URL_KEYS.REPORT_DEFINITIONS]?: Record; + time?: { + to: string; + from: string; + }; + dataType?: AppDataType; +} + +export type AllShortSeries = Record; +export type AllSeries = Record; + +export const NEW_SERIES_KEY = 'newSeriesKey'; + +export function useUrlStorage(seriesId?: string) { + const allSeriesKey = 'sr'; + const storage = useContext((UrlStorageContext as unknown) as Context); + let series: SeriesUrl = {} as SeriesUrl; + const allShortSeries = storage.get(allSeriesKey) ?? {}; + + const allSeriesIds = Object.keys(allShortSeries); + + const allSeries: AllSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allSeries[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); + + if (seriesId) { + series = allSeries?.[seriesId] ?? ({} as SeriesUrl); + } + + const setSeries = async (seriesIdN: string, newValue: SeriesUrl) => { + allShortSeries[seriesIdN] = convertToShortUrl(newValue); + allSeries[seriesIdN] = newValue; + return storage.set(allSeriesKey, allShortSeries); + }; + + const removeSeries = (seriesIdN: string) => { + delete allShortSeries[seriesIdN]; + delete allSeries[seriesIdN]; + storage.set(allSeriesKey, allShortSeries); + }; + + const firstSeriesId = allSeriesIds?.[0]; + + return { + storage, + setSeries, + removeSeries, + series, + firstSeriesId, + allSeries, + allSeriesIds, + firstSeries: allSeries?.[firstSeriesId], + }; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx new file mode 100644 index 0000000000000..dc47a0f075fe6 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useHistory } from 'react-router-dom'; +import { ThemeContext } from 'styled-components'; +import { ExploratoryView } from './exploratory_view'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../plugin'; +import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; +import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; +import { + createKbnUrlStateStorage, + withNotifyOnErrors, +} from '../../../../../../../src/plugins/kibana_utils/public/'; +import { UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { useInitExploratoryView } from './hooks/use_init_exploratory_view'; +import { WithHeaderLayout } from '../../app/layout/with_header'; + +export function ExploratoryViewPage() { + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.overview.exploratoryView', { + defaultMessage: 'Exploratory view', + }), + }, + ]); + + const theme = useContext(ThemeContext); + + const { + services: { uiSettings, notifications }, + } = useKibana(); + + const history = useHistory(); + + const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: uiSettings!.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(notifications!.toasts), + }); + + const indexPattern = useInitExploratoryView(kbnUrlStateStorage); + + return ( + + {indexPattern ? ( + + + + + + ) : null} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx new file mode 100644 index 0000000000000..112bfcc3ccb58 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -0,0 +1,318 @@ +/* + * Copyright 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 { of } from 'rxjs'; +import React, { ReactElement } from 'react'; +import { stringify } from 'query-string'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory, History } from 'history'; +import { CoreStart } from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { coreMock } from 'src/core/public/mocks'; +import { + KibanaServices, + KibanaContextProvider, +} from '../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../plugin'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { lensPluginMock } from '../../../../../lens/public/mocks'; +import { IndexPatternContextProvider } from './hooks/use_default_index_pattern'; +import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage'; +import { + withNotifyOnErrors, + createKbnUrlStateStorage, +} from '../../../../../../../src/plugins/kibana_utils/public'; +import * as fetcherHook from '../../../hooks/use_fetcher'; +import * as useUrlHook from './hooks/use_url_strorage'; +import * as useSeriesFilterHook from './hooks/use_series_filters'; +import * as useHasDataHook from '../../../hooks/use_has_data'; +import * as useValuesListHook from '../../../hooks/use_values_list'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub'; +import indexPatternData from './configurations/data/test_index_pattern.json'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { UrlFilter } from './types'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; + +interface KibanaProps { + services?: KibanaServices; +} + +export interface KibanaProviderOptions { + core?: ExtraCore & Partial; + kibanaProps?: KibanaProps; +} + +interface MockKibanaProviderProps> + extends KibanaProviderOptions { + children: ReactElement; + history: History; +} + +type MockRouterProps> = MockKibanaProviderProps; + +type Url = + | string + | { + path: string; + queryParams: Record; + }; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + renderOptions?: Omit; + url?: Url; +} + +function getSetting(key: string): T { + if (key === 'timepicker:quickRanges') { + return ([ + { + display: 'Today', + from: 'now/d', + to: 'now/d', + }, + ] as unknown) as T; + } + return ('MMM D, YYYY @ HH:mm:ss.SSS' as unknown) as T; +} + +function setSetting$(key: string): T { + return (of('MMM D, YYYY @ HH:mm:ss.SSS') as unknown) as T; +} + +/* default mock core */ +const defaultCore = coreMock.createStart(); +export const mockCore: () => Partial = () => { + const core: Partial = { + ...defaultCore, + application: { + ...defaultCore.application, + getUrlForApp: () => '/app/observability', + navigateToUrl: jest.fn(), + capabilities: { + ...defaultCore.application.capabilities, + observability: { + 'alerting:save': true, + configureSettings: true, + save: true, + show: true, + }, + }, + }, + uiSettings: { + ...defaultCore.uiSettings, + get: getSetting, + get$: setSetting$, + }, + lens: lensPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), + }; + + return core; +}; + +/* Mock Provider Components */ +export function MockKibanaProvider>({ + children, + core, + history, + kibanaProps, +}: MockKibanaProviderProps) { + const { notifications } = core!; + + const kbnUrlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + ...withNotifyOnErrors(notifications!.toasts), + }); + + const indexPattern = mockIndexPattern; + + setIndexPatterns(({ + ...[indexPattern], + get: async () => indexPattern, + } as unknown) as IndexPatternsContract); + + return ( + + + + + + {children} + + + + + + ); +} + +export function MockRouter({ + children, + core, + history = createMemoryHistory(), + kibanaProps, +}: MockRouterProps) { + return ( + + + {children} + + + ); +} + +/* Custom react testing library render */ +export function render( + ui: ReactElement, + { + history = createMemoryHistory(), + core: customCore, + kibanaProps, + renderOptions, + url, + }: RenderRouterOptions = {} +) { + if (url) { + history = getHistoryFromUrl(url); + } + + const core = { + ...mockCore(), + ...customCore, + }; + + return { + ...reactTestLibRender( + + {ui} + , + renderOptions + ), + history, + core, + }; +} + +const getHistoryFromUrl = (url: Url) => { + if (typeof url === 'string') { + return createMemoryHistory({ + initialEntries: [url], + }); + } + + return createMemoryHistory({ + initialEntries: [url.path + stringify(url.queryParams)], + }); +}; + +export const mockFetcher = (data: any) => { + return jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); +}; + +export const mockUseHasData = () => { + const onRefreshTimeRange = jest.fn(); + const spy = jest.spyOn(useHasDataHook, 'useHasData').mockReturnValue({ + onRefreshTimeRange, + } as any); + return { spy, onRefreshTimeRange }; +}; + +export const mockUseValuesList = (values?: string[]) => { + const onRefreshTimeRange = jest.fn(); + const spy = jest.spyOn(useValuesListHook, 'useValuesList').mockReturnValue({ + values: values ?? [], + } as any); + return { spy, onRefreshTimeRange }; +}; + +export const mockUrlStorage = ({ + data, + filters, + breakdown, +}: { + data?: AllSeries; + filters?: UrlFilter[]; + breakdown?: string; +}) => { + const mockDataSeries = data || { + 'performance-distribution': { + reportType: 'pld', + breakdown: breakdown || 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + ...(filters ? { filters } : {}), + }, + }; + const allSeriesIds = Object.keys(mockDataSeries); + const firstSeriesId = allSeriesIds?.[0]; + + const series = mockDataSeries[firstSeriesId]; + + const removeSeries = jest.fn(); + const setSeries = jest.fn(); + + const spy = jest.spyOn(useUrlHook, 'useUrlStorage').mockReturnValue({ + firstSeriesId, + allSeriesIds, + removeSeries, + setSeries, + series, + firstSeries: mockDataSeries[firstSeriesId], + allSeries: mockDataSeries, + } as any); + + return { spy, removeSeries, setSeries }; +}; + +export function mockUseSeriesFilter() { + const removeFilter = jest.fn(); + const invertFilter = jest.fn(); + const setFilter = jest.fn(); + const spy = jest.spyOn(useSeriesFilterHook, 'useSeriesFilters').mockReturnValue({ + removeFilter, + invertFilter, + setFilter, + }); + + return { + spy, + removeFilter, + invertFilter, + setFilter, + }; +} + +const hist = createMemoryHistory(); +export const mockHistory = { + ...hist, + createHref: jest.fn(({ pathname }) => `/observability${pathname}`), + push: jest.fn(), + location: { + ...hist.location, + pathname: '/current-path', + }, +}; + +export const mockIndexPattern = getStubIndexPattern( + 'apm-*', + () => {}, + '@timestamp', + JSON.parse(indexPatternData.attributes.fields), + mockCore() as any +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx new file mode 100644 index 0000000000000..d33d8515d3bee --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -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 React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { dataTypes, DataTypesCol } from './data_types_col'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; + +describe('DataTypesCol', function () { + it('should render properly', function () { + const { getByText } = render(); + + dataTypes.forEach(({ label }) => { + getByText(label); + }); + }); + + it('should set series on change', function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + fireEvent.click(screen.getByText(/user experience\(rum\)/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'rum' }); + }); + + it('should set series on change on already selected', function () { + const { setSeries } = mockUrlStorage({ + data: { + [NEW_SERIES_KEY]: { + dataType: 'synthetics', + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + const button = screen.getByRole('button', { + name: /Synthetic Monitoring/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: undefined }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx new file mode 100644 index 0000000000000..7ea44e66a721a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AppDataType } from '../../types'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'rum', label: 'User Experience(RUM)' }, + { id: 'logs', label: 'Logs' }, + { id: 'metrics', label: 'Metrics' }, + { id: 'apm', label: 'APM' }, +]; + +export function DataTypesCol() { + const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY); + + const { loadIndexPattern } = useIndexPatternContext(); + + const onDataTypeChange = (dataType?: AppDataType) => { + if (dataType) { + loadIndexPattern(dataType); + } + setSeries(NEW_SERIES_KEY, { dataType } as any); + }; + + const selectedDataType = series.dataType; + + return ( + + {dataTypes.map(({ id: dataTypeId, label }) => ( + + { + onDataTypeChange(dataTypeId === selectedDataType ? undefined : dataTypeId); + }} + > + {label} + + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx new file mode 100644 index 0000000000000..dba660fff9c36 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 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 { fireEvent, screen } from '@testing-library/react'; +import { render } from '../../../../../utils/test_helper'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { ReportBreakdowns } from './report_breakdowns'; +import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('Series Builder ReportBreakdowns', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + it('should render properly', function () { + mockUrlStorage({}); + + render(); + + screen.getByText('Select an option: , is selected'); + screen.getAllByText('Browser family'); + }); + + it('should set new series breakdown on change', function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/operating system/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + breakdown: USER_AGENT_OS, + reportType: 'pld', + time: { from: 'now-15m', to: 'now' }, + }); + }); + it('should set undefined on new series on no select breakdown', function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/no breakdown/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + breakdown: undefined, + reportType: 'pld', + time: { from: 'now-15m', to: 'now' }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx new file mode 100644 index 0000000000000..7667cea417a52 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Breakdowns } from '../../series_editor/columns/breakdowns'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { DataSeries } from '../../types'; + +export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) { + return ; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx new file mode 100644 index 0000000000000..2fda581154166 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 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 { fireEvent, screen } from '@testing-library/react'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { ReportDefinitionCol } from './report_definition_col'; +import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('Series Builder ReportDefinitionCol', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + const { setSeries } = mockUrlStorage({ + data: { + 'performance-dist': { + dataType: 'rum', + reportType: 'pld', + time: { from: 'now-30d', to: 'now' }, + reportDefinitions: { [SERVICE_NAME]: 'elastic-co' }, + }, + }, + }); + + it('should render properly', async function () { + render(); + + screen.getByText('Web Application'); + screen.getByText('Environment'); + screen.getByText('Select an option: Page load time, is selected'); + screen.getByText('Page load time'); + }); + + it('should render selected report definitions', function () { + render(); + + screen.getByText('elastic-co'); + }); + + it('should be able to remove selected definition', function () { + render(); + + const removeBtn = screen.getByText(/elastic-co/i); + + fireEvent.click(removeBtn); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + dataType: 'rum', + reportDefinitions: {}, + reportType: 'pld', + time: { from: 'now-30d', to: 'now' }, + }); + }); + + it('should be able to unselected selected definition', async function () { + mockUseValuesList(['elastic-co']); + render(); + + const definitionBtn = screen.getByText(/web application/i); + + fireEvent.click(definitionBtn); + + screen.getByText('Apply'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx new file mode 100644 index 0000000000000..ce11c869de0ab --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.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 React from 'react'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; +import { CustomReportField } from '../custom_report_field'; +import FieldValueSuggestions from '../../../field_value_suggestions'; +import { DataSeries } from '../../types'; + +export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSeries }) { + const { indexPattern } = useIndexPatternContext(); + + const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY); + + const { reportDefinitions: rtd = {} } = series; + + const { reportDefinitions, labels, filters } = dataViewSeries; + + const onChange = (field: string, value?: string) => { + if (!value) { + delete rtd[field]; + setSeries(NEW_SERIES_KEY, { + ...series, + reportDefinitions: { ...rtd }, + }); + } else { + setSeries(NEW_SERIES_KEY, { + ...series, + reportDefinitions: { ...rtd, [field]: value }, + }); + } + }; + + const onRemove = (field: string) => { + delete rtd[field]; + setSeries(NEW_SERIES_KEY, { + ...series, + reportDefinitions: rtd, + }); + }; + + return ( + + {indexPattern && + reportDefinitions.map(({ field, custom, options, defaultValue }) => ( + + {!custom ? ( + + + onChange(field, val)} + filters={(filters ?? []).map(({ query }) => query)} + time={series.time} + width={200} + /> + + {rtd?.[field] && ( + + onRemove(field)} + iconOnClick={() => onRemove(field)} + iconOnClickAriaLabel={'Click to remove'} + onClickAriaLabel={'Click to remove'} + > + {rtd?.[field]} + + + )} + + ) : ( + + )} + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx new file mode 100644 index 0000000000000..674f5e6f49bde --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.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 React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from '../../../../../utils/test_helper'; +import { ReportFilters } from './report_filters'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; + +describe('Series Builder ReportFilters', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + mockUrlStorage({}); + it('should render properly', function () { + render(); + + screen.getByText('Add filter'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx new file mode 100644 index 0000000000000..903dda549aeee --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SeriesFilter } from '../../series_editor/columns/series_filter'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { DataSeries } from '../../types'; + +export function ReportFilters({ dataViewSeries }: { dataViewSeries: DataSeries }) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx new file mode 100644 index 0000000000000..567e2654130e8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; +import { ReportTypes } from '../series_builder'; + +describe('ReportTypesCol', function () { + it('should render properly', function () { + render(); + screen.getByText('Performance distribution'); + screen.getByText('KPI over time'); + }); + + it('should display empty message', function () { + render(); + screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); + }); + + it('should set series on change', function () { + const { setSeries } = mockUrlStorage({}); + render(); + + fireEvent.click(screen.getByText(/monitor duration/i)); + + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { + breakdown: 'user_agent.name', + reportDefinitions: {}, + reportType: 'upd', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); + + it('should set selected as filled', function () { + const { setSeries } = mockUrlStorage({ + data: { + newSeriesKey: { + dataType: 'synthetics', + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + const button = screen.getByRole('button', { + name: /pings histogram/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'synthetics' }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx new file mode 100644 index 0000000000000..5c94a5bca60f8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { ReportViewTypeId, SeriesUrl } from '../../types'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + reportTypes: Array<{ id: ReportViewTypeId; label: string }>; +} + +export function ReportTypesCol({ reportTypes }: Props) { + const { + series: { reportType: selectedReportType, ...restSeries }, + setSeries, + } = useUrlStorage(NEW_SERIES_KEY); + + return reportTypes?.length > 0 ? ( + + {reportTypes.map(({ id: reportType, label }) => ( + + { + if (reportType === selectedReportType) { + setSeries(NEW_SERIES_KEY, { + dataType: restSeries.dataType, + } as SeriesUrl); + } else { + setSeries(NEW_SERIES_KEY, { + ...restSeries, + reportType, + reportDefinitions: {}, + }); + } + }} + > + {label} + + + ))} + + ) : ( + {SELECTED_DATA_TYPE_FOR_REPORT} + ); +} + +export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( + 'xpack.observability.expView.reportType.noDataType', + { defaultMessage: 'Select a data type to start building a series.' } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.tsx new file mode 100644 index 0000000000000..6039fd4cba280 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/custom_report_field.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 { EuiSuperSelect } from '@elastic/eui'; +import { useUrlStorage } from '../hooks/use_url_strorage'; +import { ReportDefinition } from '../types'; + +interface Props { + field: string; + seriesId: string; + defaultValue?: string; + options: ReportDefinition['options']; +} + +export function CustomReportField({ field, seriesId, options: opts, defaultValue }: Props) { + const { series, setSeries } = useUrlStorage(seriesId); + + const { reportDefinitions: rtd = {} } = series; + + const onChange = (value: string) => { + setSeries(seriesId, { ...series, reportDefinitions: { ...rtd, [field]: value } }); + }; + + const { reportDefinitions } = series; + + const NO_SELECT = 'no_select'; + + const options = [{ label: 'Select metric', field: NO_SELECT }, ...(opts ?? [])]; + + return ( +
+ ({ + value: fd, + inputDisplay: label, + }))} + valueOfSelected={reportDefinitions?.[field] || defaultValue || NO_SELECT} + onChange={(value) => onChange(value)} + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx new file mode 100644 index 0000000000000..983c18af031d0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiBasicTable, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types'; +import { DataTypesCol } from './columns/data_types_col'; +import { ReportTypesCol } from './columns/report_types_col'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { ReportFilters } from './columns/report_filters'; +import { ReportBreakdowns } from './columns/report_breakdowns'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; +import { getDefaultConfigs } from '../configurations/default_configs'; + +export const ReportTypes: Record> = { + synthetics: [ + { id: 'upd', label: 'Monitor duration' }, + { id: 'upp', label: 'Pings histogram' }, + ], + rum: [ + { id: 'pld', label: 'Performance distribution' }, + { id: 'kpi', label: 'KPI over time' }, + ], + apm: [ + { id: 'svl', label: 'Latency' }, + { id: 'tpt', label: 'Throughput' }, + ], + logs: [ + { + id: 'logs', + label: 'Logs Frequency', + }, + ], + metrics: [ + { id: 'cpu', label: 'CPU usage' }, + { id: 'mem', label: 'Memory usage' }, + { id: 'nwk', label: 'Network activity' }, + ], +}; + +export function SeriesBuilder() { + const { series, setSeries, allSeriesIds, removeSeries } = useUrlStorage(NEW_SERIES_KEY); + + const { dataType, reportType, reportDefinitions = {}, filters = [] } = series; + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(!!series.dataType); + + const { indexPattern } = useIndexPatternContext(); + + const getDataViewSeries = () => { + return getDefaultConfigs({ + indexPattern, + reportType: reportType!, + seriesId: NEW_SERIES_KEY, + }); + }; + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { + defaultMessage: 'Data Type', + }), + width: '20%', + render: (val: string) => , + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { + defaultMessage: 'Report', + }), + width: '20%', + render: (val: string) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { + defaultMessage: 'Definition', + }), + width: '30%', + render: (val: string) => + reportType && indexPattern ? ( + + ) : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { + defaultMessage: 'Filters', + }), + width: '25%', + render: (val: string) => + reportType && indexPattern ? : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { + defaultMessage: 'Breakdowns', + }), + width: '25%', + field: 'id', + render: (val: string) => + reportType && indexPattern ? ( + + ) : null, + }, + ]; + + const addSeries = () => { + if (reportType) { + const newSeriesId = `${ + reportDefinitions?.['service.name'] || + reportDefinitions?.['monitor.id'] || + ReportViewTypes[reportType] + }`; + + const newSeriesN = { + reportType, + time: { from: 'now-30m', to: 'now' }, + filters, + reportDefinitions, + } as SeriesUrl; + + setSeries(newSeriesId, newSeriesN).then(() => { + removeSeries(NEW_SERIES_KEY); + setIsFlyoutVisible(false); + }); + } + }; + + const items = [{ id: NEW_SERIES_KEY }]; + + let flyout; + + if (isFlyoutVisible) { + flyout = ( + + + + + + + {i18n.translate('xpack.observability.expView.seriesBuilder.add', { + defaultMessage: 'Add', + })} + + + + { + removeSeries(NEW_SERIES_KEY); + setIsFlyoutVisible(false); + }} + > + {i18n.translate('xpack.observability.expView.seriesBuilder.cancel', { + defaultMessage: 'Cancel', + })} + + + + + ); + } + + return ( +
+ {!isFlyoutVisible && ( + <> + setIsFlyoutVisible((prevState) => !prevState)} + disabled={allSeriesIds.length > 0} + > + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add series', + })} + + + + )} + {flyout} +
+ ); +} + +const BottomFlyout = styled.div` + height: 300px; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx new file mode 100644 index 0000000000000..71e3317ad6db8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSuperDatePicker } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useUrlStorage } from '../hooks/use_url_strorage'; +import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: string; +} + +export function SeriesDatePicker({ seriesId }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { series, setSeries } = useUrlStorage(seriesId); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + + useEffect(() => { + if (!series || !series.time) { + setSeries(seriesId, { ...series, time: { from: 'now-5h', to: 'now' } }); + } + }, [seriesId, series, setSeries]); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx new file mode 100644 index 0000000000000..acc9ba9658a08 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.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 from 'react'; +import { mockUrlStorage, mockUseHasData, render } from '../rtl_helpers'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { SeriesDatePicker } from './index'; + +describe('SeriesDatePicker', function () { + it('should render properly', function () { + mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-30m', to: 'now' }, + }, + }, + }); + const { getByText } = render(); + + getByText('Last 30 minutes'); + }); + + it('should set defaults', async function () { + const { setSeries: setSeries1 } = mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + }, + }, + } as any); + render(); + expect(setSeries1).toHaveBeenCalledTimes(1); + expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { + breakdown: 'monitor.status', + reportType: 'upp', + time: { from: 'now-5h', to: 'now' }, + }); + }); + + it('should set series data', async function () { + const { setSeries } = mockUrlStorage({ + data: { + 'uptime-pings-histogram': { + reportType: 'upp', + breakdown: 'monitor.status', + time: { from: 'now-30m', to: 'now' }, + }, + }, + }); + + const { onRefreshTimeRange } = mockUseHasData(); + const { getByTestId } = render(); + + await waitFor(function () { + fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); + }); + + fireEvent.click(getByTestId('superDatePickerCommonlyUsed_Today')); + + expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); + + expect(setSeries).toHaveBeenCalledWith('series-id', { + breakdown: 'monitor.status', + reportType: 'upp', + time: { from: 'now/d', to: 'now/d' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx new file mode 100644 index 0000000000000..c6209381a4da1 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/actions_col.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { DataSeries } from '../../types'; +import { SeriesChartTypes } from './chart_types'; +import { MetricSelection } from './metric_selection'; + +interface Props { + series: DataSeries; +} + +export function ActionsCol({ series }: Props) { + return ( + + + + + {series.hasMetricType && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx new file mode 100644 index 0000000000000..654a93a08a7c8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.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 { fireEvent, screen } from '@testing-library/react'; +import { Breakdowns } from './breakdowns'; +import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers'; +import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('Breakdowns', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + it('should render properly', async function () { + mockUrlStorage({}); + + render(); + + screen.getAllByText('Browser family'); + }); + + it('should call set series on change', function () { + const { setSeries } = mockUrlStorage({ breakdown: USER_AGENT_OS }); + + render(); + + screen.getAllByText('Operating system'); + + fireEvent.click(screen.getByTestId('seriesBreakdown')); + + fireEvent.click(screen.getByText('Browser family')); + + expect(setSeries).toHaveBeenCalledWith('series-id', { + breakdown: 'user_agent.name', + reportType: 'pld', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx new file mode 100644 index 0000000000000..0d34d7245725a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FieldLabels } from '../../configurations/constants'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + seriesId: string; + breakdowns: string[]; +} + +export function Breakdowns({ seriesId, breakdowns = [] }: Props) { + const { setSeries, series } = useUrlStorage(seriesId); + + const selectedBreakdown = series.breakdown; + const NO_BREAKDOWN = 'no_breakdown'; + + const onOptionChange = (optionId: string) => { + if (optionId === NO_BREAKDOWN) { + setSeries(seriesId, { + ...series, + breakdown: undefined, + }); + } else { + setSeries(seriesId, { + ...series, + breakdown: selectedBreakdown === optionId ? undefined : optionId, + }); + } + }; + + const items = breakdowns.map((breakdown) => ({ id: breakdown, label: FieldLabels[breakdown] })); + items.push({ + id: NO_BREAKDOWN, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), + }); + + const options = items.map(({ id, label }) => ({ + inputDisplay: id === NO_BREAKDOWN ? label : {label}, + value: id, + dropdownDisplay: label, + })); + + return ( +
+ onOptionChange(value)} + data-test-subj={'seriesBreakdown'} + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx new file mode 100644 index 0000000000000..f291d0de4dac0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { SeriesChartTypes, XYChartTypes } from './chart_types'; +import { mockUrlStorage, render } from '../../rtl_helpers'; + +describe.skip('SeriesChartTypes', function () { + it('should render properly', async function () { + mockUrlStorage({}); + + render(); + + await waitFor(() => { + screen.getByText(/chart type/i); + }); + }); + + it('should call set series on change', async function () { + const { setSeries } = mockUrlStorage({}); + + render(); + + await waitFor(() => { + screen.getByText(/chart type/i); + }); + + fireEvent.click(screen.getByText(/chart type/i)); + fireEvent.click(screen.getByTestId('lnsXY_seriesType-bar_stacked')); + + expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', { + breakdown: 'user_agent.name', + reportType: 'pld', + seriesType: 'bar_stacked', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(3); + }); + + describe('XYChartTypes', function () { + it('should render properly', async function () { + mockUrlStorage({}); + + render(); + + await waitFor(() => { + screen.getByText(/chart type/i); + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx new file mode 100644 index 0000000000000..017655053eef2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { + EuiButton, + EuiButtonGroup, + EuiButtonIcon, + EuiLoadingSpinner, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; +import { useFetcher } from '../../../../..'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { SeriesType } from '../../../../../../../lens/public'; + +export function SeriesChartTypes({ + seriesId, + defaultChartType, +}: { + seriesId: string; + defaultChartType: SeriesType; +}) { + const { series, setSeries, allSeries } = useUrlStorage(seriesId); + + const seriesType = series?.seriesType ?? defaultChartType; + + const onChange = (value: SeriesType) => { + Object.keys(allSeries).forEach((seriesKey) => { + const seriesN = allSeries[seriesKey]; + + setSeries(seriesKey, { ...seriesN, seriesType: value }); + }); + }; + + return ( + + ); +} + +export interface XYChartTypesProps { + onChange: (value: SeriesType) => void; + value: SeriesType; + label?: string; + includeChartTypes?: string[]; + excludeChartTypes?: string[]; +} + +export function XYChartTypes({ + onChange, + value, + label, + includeChartTypes, + excludeChartTypes, +}: XYChartTypesProps) { + const [isOpen, setIsOpen] = useState(false); + + const { + services: { lens }, + } = useKibana(); + + const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]); + + let vizTypes = data ?? []; + + if ((excludeChartTypes ?? []).length > 0) { + vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id)); + } + + if ((includeChartTypes ?? []).length > 0) { + vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id)); + } + + return loading ? ( + + ) : ( + id === value)?.icon} + onClick={() => { + setIsOpen((prevState) => !prevState); + }} + > + {label} + + ) : ( + id === value)?.label} + iconType={vizTypes.find(({ id }) => id === value)?.icon!} + onClick={() => { + setIsOpen((prevState) => !prevState); + }} + /> + ) + } + closePopover={() => setIsOpen(false)} + > + ({ + id: t.id, + label: t.label, + title: t.label, + iconType: t.icon || 'empty', + 'data-test-subj': `lnsXY_seriesType-${t.id}`, + }))} + idSelected={value} + onChange={(valueN: string) => { + onChange(valueN as SeriesType); + }} + /> + + ); +} + +const ButtonGroup = styled(EuiButtonGroup)` + &&& { + .euiButtonGroupButton-isSelected { + background-color: #a5a9b1 !important; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx new file mode 100644 index 0000000000000..8c99de51978a7 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SeriesDatePicker } from '../../series_date_picker'; + +interface Props { + seriesId: string; +} +export function DatePickerCol({ seriesId }: Props) { + return ( +
+ +
+ ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx new file mode 100644 index 0000000000000..edd5546f13940 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { FilterExpanded } from './filter_expanded'; +import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers'; +import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames'; + +describe('FilterExpanded', function () { + it('should render properly', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + render( + + ); + + screen.getByText('Browser Family'); + }); + it('should call go back on click', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + const goBack = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('Browser Family')); + + expect(goBack).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledWith(); + }); + + it('should call useValuesList on load', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + const { spy } = mockUseValuesList(['Chrome', 'Firefox']); + + const goBack = jest.fn(); + + render( + + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toBeCalledWith( + expect.objectContaining({ + time: { from: 'now-15m', to: 'now' }, + sourceField: USER_AGENT_NAME, + }) + ); + }); + it('should filter display values', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + mockUseValuesList(['Chrome', 'Firefox']); + + render( + + ); + + expect(screen.queryByText('Firefox')).toBeTruthy(); + + fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); + + expect(screen.queryByText('Firefox')).toBeFalsy(); + expect(screen.getByText('Chrome')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx new file mode 100644 index 0000000000000..280912dd0902f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 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, Fragment } from 'react'; +import { + EuiFieldSearch, + EuiSpacer, + EuiButtonEmpty, + EuiLoadingSpinner, + EuiFilterGroup, +} from '@elastic/eui'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { UrlFilter } from '../../types'; +import { FilterValueButton } from './filter_value_btn'; +import { useValuesList } from '../../../../../hooks/use_values_list'; + +interface Props { + seriesId: string; + label: string; + field: string; + goBack: () => void; + nestedField?: string; +} + +export function FilterExpanded({ seriesId, field, label, goBack, nestedField }: Props) { + const { indexPattern } = useIndexPatternContext(); + + const [value, setValue] = useState(''); + + const [isOpen, setIsOpen] = useState({ value: '', negate: false }); + + const { series } = useUrlStorage(seriesId); + + const { values, loading } = useValuesList({ + sourceField: field, + time: series.time, + indexPattern, + }); + + const filters = series?.filters ?? []; + + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); + + const displayValues = (values || []).filter((opt) => + opt.toLowerCase().includes(value.toLowerCase()) + ); + + return ( + <> + goBack()}> + {label} + + { + setValue(evt.target.value); + }} + /> + + {loading && ( +
+ +
+ )} + {displayValues.map((opt) => ( + + + + + + + + ))} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx new file mode 100644 index 0000000000000..7f76c9ea999ee --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { FilterValueButton } from './filter_value_btn'; +import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { + USER_AGENT_NAME, + USER_AGENT_VERSION, +} from '../../configurations/data/elasticsearch_fieldnames'; + +describe('FilterValueButton', function () { + it('should render properly', async function () { + render( + + ); + + screen.getByText('Chrome'); + }); + + it('should render display negate state', async function () { + render( + + ); + + screen.getByText('Not Chrome'); + screen.getByTitle('Not Chrome'); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); + }); + + it('should call set filter on click', async function () { + const { setFilter, removeFilter } = mockUseSeriesFilter(); + + render( + + ); + + fireEvent.click(screen.getByText('Not Chrome')); + + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', + }); + }); + it('should remove filter on click if already selected', async function () { + mockUrlStorage({}); + const { removeFilter } = mockUseSeriesFilter(); + + render( + + ); + + fireEvent.click(screen.getByText('Chrome')); + + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', + }); + }); + + it('should change filter on negated one', async function () { + const { removeFilter } = mockUseSeriesFilter(); + + render( + + ); + + fireEvent.click(screen.getByText('Not Chrome')); + + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', + }); + }); + + it('should force open nested', async function () { + mockUseSeriesFilter(); + const { spy } = mockUseValuesList(); + + render( + + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toBeCalledWith( + expect.objectContaining({ + filters: [ + { + term: { + [USER_AGENT_NAME]: 'Chrome', + }, + }, + ], + sourceField: 'user_agent.version', + }) + ); + }); + it('should set isNestedOpen on click', async function () { + mockUseSeriesFilter(); + const { spy } = mockUseValuesList(); + + render( + + ); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toBeCalledWith( + expect.objectContaining({ + filters: [ + { + term: { + [USER_AGENT_NAME]: 'Chrome', + }, + }, + ], + sourceField: USER_AGENT_VERSION, + }) + ); + }); + + it('should set call setIsNestedOpen on click selected', async function () { + mockUseSeriesFilter(); + mockUseValuesList(); + + const setIsNestedOpen = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('Chrome')); + + expect(setIsNestedOpen).toHaveBeenCalledTimes(1); + expect(setIsNestedOpen).toHaveBeenCalledWith({ negate: false, value: '' }); + }); + + it('should set call setIsNestedOpen on click not selected', async function () { + mockUseSeriesFilter(); + mockUseValuesList(); + + const setIsNestedOpen = jest.fn(); + + render( + + ); + + fireEvent.click(screen.getByText('Not Chrome')); + + expect(setIsNestedOpen).toHaveBeenCalledTimes(1); + expect(setIsNestedOpen).toHaveBeenCalledWith({ negate: true, value: 'Chrome' }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx new file mode 100644 index 0000000000000..42cdfd595e66b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 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 React, { useMemo } from 'react'; +import { EuiFilterButton, hexToRgb } from '@elastic/eui'; +import { useIndexPatternContext } from '../../hooks/use_default_index_pattern'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { useSeriesFilters } from '../../hooks/use_series_filters'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import FieldValueSuggestions from '../../../field_value_suggestions'; + +interface Props { + value: string; + field: string; + allSelectedValues?: string[]; + negate: boolean; + nestedField?: string; + seriesId: string; + isNestedOpen: { + value: string; + negate: boolean; + }; + setIsNestedOpen: (val: { value: string; negate: boolean }) => void; +} + +export function FilterValueButton({ + isNestedOpen, + setIsNestedOpen, + value, + field, + negate, + seriesId, + nestedField, + allSelectedValues, +}: Props) { + const { series } = useUrlStorage(seriesId); + + const { indexPattern } = useIndexPatternContext(); + + const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); + + const hasActiveFilters = (allSelectedValues ?? []).includes(value); + + const button = ( + { + if (hasActiveFilters) { + removeFilter({ field, value, negate }); + } else { + setFilter({ field, value, negate }); + } + if (!hasActiveFilters) { + setIsNestedOpen({ value, negate }); + } else { + setIsNestedOpen({ value: '', negate }); + } + }} + > + {negate + ? i18n.translate('xpack.observability.expView.filterValueButton.negate', { + defaultMessage: 'Not {value}', + values: { value }, + }) + : value} + + ); + + const onNestedChange = (val?: string) => { + setFilter({ field: nestedField!, value: val! }); + setIsNestedOpen({ value: '', negate }); + }; + + const forceOpenNested = isNestedOpen?.value === value && isNestedOpen.negate === negate; + + const filters = useMemo(() => { + return [ + { + term: { + [field]: value, + }, + }, + ]; + }, [field, value]); + + return nestedField && forceOpenNested ? ( + + ) : ( + button + ); +} + +const FilterButton = euiStyled(EuiFilterButton)` + background-color: rgba(${(props) => { + const color = props.hasActiveFilters + ? props.color === 'danger' + ? hexToRgb(props.theme.eui.euiColorDanger) + : hexToRgb(props.theme.eui.euiColorPrimary) + : 'initial'; + return `${color[0]}, ${color[1]}, ${color[2]}, 0.1`; + }}); +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx new file mode 100644 index 0000000000000..ced04f0a59c8c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright 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 { fireEvent, screen } from '@testing-library/react'; +import { mockUrlStorage, render } from '../../rtl_helpers'; +import { MetricSelection } from './metric_selection'; + +describe('MetricSelection', function () { + it('should render properly', function () { + render(); + + screen.getByText('Average'); + }); + + it('should display selected value', function () { + mockUrlStorage({ + data: { + 'performance-distribution': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + screen.getByText('Median'); + }); + + it('should be disabled on disabled state', function () { + render(); + + const btn = screen.getByRole('button'); + + expect(btn.classList).toContain('euiButton-isDisabled'); + }); + + it('should call set series on change', function () { + const { setSeries } = mockUrlStorage({ + data: { + 'performance-distribution': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + fireEvent.click(screen.getByText('Median')); + + screen.getByText('Chart metric group'); + + fireEvent.click(screen.getByText('95th Percentile')); + + expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', { + metric: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times + // This should be one https://github.com/elastic/eui/issues/4629 + expect(setSeries).toHaveBeenCalledTimes(3); + }); + + it('should call set series on change for all series', function () { + const { setSeries } = mockUrlStorage({ + data: { + 'page-views': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + 'performance-distribution': { + reportType: 'kpi', + metric: 'median', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }); + + render(); + + fireEvent.click(screen.getByText('Median')); + + screen.getByText('Chart metric group'); + + fireEvent.click(screen.getByText('95th Percentile')); + + expect(setSeries).toHaveBeenNthCalledWith(1, 'page-views', { + metric: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + + expect(setSeries).toHaveBeenNthCalledWith(2, 'performance-distribution', { + metric: '95th', + reportType: 'kpi', + time: { from: 'now-15m', to: 'now' }, + }); + // FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times + // This should be one https://github.com/elastic/eui/issues/4629 + expect(setSeries).toHaveBeenCalledTimes(6); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.tsx new file mode 100644 index 0000000000000..e01e371b5eeeb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/metric_selection.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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; +import { OperationType } from '../../../../../../../lens/public'; + +const toggleButtons = [ + { + id: `avg`, + label: i18n.translate('xpack.observability.expView.metricsSelect.average', { + defaultMessage: 'Average', + }), + }, + { + id: `median`, + label: i18n.translate('xpack.observability.expView.metricsSelect.median', { + defaultMessage: 'Median', + }), + }, + { + id: `95th`, + label: i18n.translate('xpack.observability.expView.metricsSelect.9thPercentile', { + defaultMessage: '95th Percentile', + }), + }, + { + id: `99th`, + label: i18n.translate('xpack.observability.expView.metricsSelect.99thPercentile', { + defaultMessage: '99th Percentile', + }), + }, +]; + +export function MetricSelection({ + seriesId, + isDisabled, +}: { + seriesId: string; + isDisabled: boolean; +}) { + const { series, setSeries, allSeries } = useUrlStorage(seriesId); + + const [isOpen, setIsOpen] = useState(false); + + const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg'); + + const onChange = (optionId: OperationType) => { + setToggleIdSelected(optionId); + + Object.keys(allSeries).forEach((seriesKey) => { + const seriesN = allSeries[seriesKey]; + + setSeries(seriesKey, { ...seriesN, metric: optionId }); + }); + }; + const button = ( + setIsOpen((prevState) => !prevState)} + size="s" + color="text" + isDisabled={isDisabled} + > + {toggleButtons.find(({ id }) => id === toggleIdSelected)!.label} + + ); + + return ( + setIsOpen(false)}> + onChange(id as OperationType)} + /> + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx new file mode 100644 index 0000000000000..67aebed943326 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -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 React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { DataSeries } from '../../types'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + series: DataSeries; +} + +export function RemoveSeries({ series }: Props) { + const { removeSeries } = useUrlStorage(); + + const onClick = () => { + removeSeries(series.id); + }; + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx new file mode 100644 index 0000000000000..24b65d2adb38e --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -0,0 +1,139 @@ +/* + * Copyright 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 React, { useState, Fragment } from 'react'; +import { + EuiButton, + EuiPopover, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { FilterExpanded } from './filter_expanded'; +import { DataSeries } from '../../types'; +import { FieldLabels } from '../../configurations/constants'; +import { SelectedFilters } from '../selected_filters'; +import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage'; + +interface Props { + seriesId: string; + defaultFilters: DataSeries['defaultFilters']; + series: DataSeries; + isNew?: boolean; +} + +export interface Field { + label: string; + field: string; + nested?: string; +} + +export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: Props) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + + const [selectedField, setSelectedField] = useState(); + + const options = defaultFilters.map((field) => { + if (typeof field === 'string') { + return { label: FieldLabels[field], field }; + } + return { label: FieldLabels[field.field], field: field.field, nested: field.nested }; + }); + const disabled = seriesId === NEW_SERIES_KEY && !isNew; + + const { setSeries, series: urlSeries } = useUrlStorage(seriesId); + + const button = ( + { + setIsPopoverVisible(true); + }} + isDisabled={disabled} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { + defaultMessage: 'Add filter', + })} + + ); + + const mainPanel = ( + <> + + {options.map((opt) => ( + + { + setSelectedField(opt); + }} + > + {opt.label} + + + + ))} + + ); + + const childPanel = selectedField ? ( + { + setSelectedField(undefined); + }} + /> + ) : null; + + const closePopover = () => { + setIsPopoverVisible(false); + setSelectedField(undefined); + }; + + return ( + + {!disabled && } + + + {!selectedField ? mainPanel : childPanel} + + + {(urlSeries.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...urlSeries, filters: undefined }); + }} + isDisabled={disabled} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx new file mode 100644 index 0000000000000..5770a7e209f06 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 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 { screen, waitFor } from '@testing-library/react'; +import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers'; +import { SelectedFilters } from './selected_filters'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { NEW_SERIES_KEY } from '../hooks/use_url_strorage'; +import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames'; + +describe('SelectedFilters', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'pld', + indexPattern: mockIndexPattern, + seriesId: NEW_SERIES_KEY, + }); + + it('should render properly', async function () { + mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }); + + render(); + + await waitFor(() => { + screen.getByText('Chrome'); + screen.getByTitle('Filter: Browser family: Chrome. Select for more filter actions.'); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx new file mode 100644 index 0000000000000..be8b1feb4d723 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.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 React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { FilterLabel } from '../components/filter_label'; +import { DataSeries, UrlFilter } from '../types'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; + +interface Props { + seriesId: string; + series: DataSeries; + isNew?: boolean; +} +export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) { + const { series } = useUrlStorage(seriesId); + + const { reportDefinitions = {} } = series; + + const { labels } = dataSeries; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions, dataSeries); + + // we don't want to display report definition filters in new series view + if (seriesId === NEW_SERIES_KEY && isNew) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId }); + + const { indexPattern } = useIndexPatternContext(); + + return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( + + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: false })} + negate={false} + /> + + ))} + {(notValues ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: true })} + /> + + ))} + + ))} + + {definitionFilters.map(({ field, values }) => ( + + {(values ?? []).map((val) => ( + + { + // FIXME handle this use case + }} + negate={false} + definitionFilter={true} + /> + + ))} + + ))} + + + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx new file mode 100644 index 0000000000000..2d423c9aee3fc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -0,0 +1,139 @@ +/* + * Copyright 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 { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { SeriesFilter } from './columns/series_filter'; +import { ActionsCol } from './columns/actions_col'; +import { Breakdowns } from './columns/breakdowns'; +import { DataSeries } from '../types'; +import { SeriesBuilder } from '../series_builder/series_builder'; +import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { DatePickerCol } from './columns/date_picker_col'; +import { RemoveSeries } from './columns/remove_series'; +import { useIndexPatternContext } from '../hooks/use_default_index_pattern'; + +export function SeriesEditor() { + const { allSeries, firstSeriesId } = useUrlStorage(); + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesEditor.name', { + defaultMessage: 'Name', + }), + field: 'id', + width: '15%', + render: (val: string) => ( + + {' '} + {val === NEW_SERIES_KEY ? 'new-series-preview' : val} + + ), + }, + ...(firstSeriesId !== NEW_SERIES_KEY + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', + }), + field: 'defaultFilters', + width: '25%', + render: (defaultFilters: string[], series: DataSeries) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', + }), + field: 'breakdowns', + width: '15%', + render: (val: string[], item: DataSeries) => ( + + ), + }, + { + name: '', + align: 'center' as const, + width: '15%', + field: 'id', + render: (val: string, item: DataSeries) => , + }, + ] + : []), + { + name: ( +
+ {i18n.translate('xpack.observability.expView.seriesEditor.time', { + defaultMessage: 'Time', + })} +
+ ), + width: '20%', + field: 'id', + align: 'right' as const, + render: (val: string, item: DataSeries) => , + }, + + ...(firstSeriesId !== NEW_SERIES_KEY + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '5%', + field: 'id', + render: (val: string, item: DataSeries) => , + }, + ] + : []), + ]; + + const allSeriesKeys = Object.keys(allSeries); + + const items: DataSeries[] = []; + + const { indexPattern } = useIndexPatternContext(); + + allSeriesKeys.forEach((seriesKey) => { + const series = allSeries[seriesKey]; + if (series.reportType && indexPattern) { + items.push( + getDefaultConfigs({ + indexPattern, + reportType: series.reportType, + seriesId: seriesKey, + }) + ); + } + }); + + return ( + <> + + (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })} + noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + defaultMessage: 'No series found, please add a series.', + })} + cellProps={{ + style: { + verticalAlign: 'top', + }, + }} + /> + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts new file mode 100644 index 0000000000000..444e0ddaecb4a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -0,0 +1,89 @@ +/* + * Copyright 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 { PaletteOutput } from 'src/plugins/charts/public'; +import { + LastValueIndexPatternColumn, + DateHistogramIndexPatternColumn, + SeriesType, + OperationType, + IndexPatternColumn, +} from '../../../../../lens/public'; + +import { PersistableFilter } from '../../../../../lens/common'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; + +export const ReportViewTypes = { + pld: 'page-load-dist', + kpi: 'kpi-trends', + upd: 'uptime-duration', + upp: 'uptime-pings', + svl: 'service-latency', + tpt: 'service-throughput', + logs: 'logs-frequency', + cpu: 'cpu-usage', + mem: 'memory-usage', + nwk: 'network-activity', +} as const; + +type ValueOf = T[keyof T]; + +export type ReportViewTypeId = keyof typeof ReportViewTypes; + +export type ReportViewType = ValueOf; + +export interface ReportDefinition { + field: string; + required?: boolean; + custom?: boolean; + defaultValue?: string; + options?: Array<{ field: string; label: string; description?: string }>; +} + +export interface DataSeries { + reportType: ReportViewType; + id: string; + xAxisColumn: Partial | Partial; + yAxisColumn: Partial; + + breakdowns: string[]; + defaultSeriesType: SeriesType; + defaultFilters: Array; + seriesTypes: SeriesType[]; + filters?: PersistableFilter[]; + reportDefinitions: ReportDefinition[]; + labels: Record; + hasMetricType: boolean; + palette?: PaletteOutput; +} + +export interface SeriesUrl { + time: { + to: string; + from: string; + }; + breakdown?: string; + filters?: UrlFilter[]; + seriesType?: SeriesType; + reportType: ReportViewTypeId; + metric?: OperationType; + dataType?: AppDataType; + reportDefinitions?: Record; +} + +export interface UrlFilter { + field: string; + values?: string[]; + notValues?: string[]; +} + +export interface ConfigProps { + seriesId: string; + indexPattern: IIndexPattern; +} + +export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm'; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index b2c682dc58937..a44aab2da85be 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -15,14 +15,19 @@ import { EuiSelectableOption, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { PopoverAnchorPosition } from '@elastic/eui/src/components/popover/popover'; export interface FieldValueSelectionProps { value?: string; label: string; - loading: boolean; + loading?: boolean; onChange: (val?: string) => void; values?: string[]; setQuery: Dispatch>; + anchorPosition?: PopoverAnchorPosition; + forceOpen?: boolean; + button?: JSX.Element; + width?: number; } const formatOptions = (values?: string[], value?: string): EuiSelectableOption[] => { @@ -38,6 +43,10 @@ export function FieldValueSelection({ loading, values, setQuery, + button, + width, + forceOpen, + anchorPosition, onChange: onSelectionChange, }: FieldValueSelectionProps) { const [options, setOptions] = useState(formatOptions(values, value)); @@ -63,8 +72,9 @@ export function FieldValueSelection({ setQuery((evt.target as HTMLInputElement).value); }; - const button = ( + const anchorButton = ( void; + filters: ESFilter[]; + anchorPosition?: PopoverAnchorPosition; + time?: { from: string; to: string }; + forceOpen?: boolean; + button?: JSX.Element; + width?: number; } export function FieldValueSuggestions({ @@ -25,12 +33,18 @@ export function FieldValueSuggestions({ label, indexPattern, value, + filters, + button, + time, + width, + forceOpen, + anchorPosition, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { const [query, setQuery] = useState(''); const [debouncedValue, setDebouncedValue] = useState(''); - const { values, loading } = useValuesList({ indexPattern, query, sourceField }); + const { values, loading } = useValuesList({ indexPattern, query, sourceField, filters, time }); useDebounce( () => { @@ -48,6 +62,10 @@ export function FieldValueSuggestions({ setQuery={setDebouncedValue} loading={loading} value={value} + button={button} + forceOpen={forceOpen} + anchorPosition={anchorPosition} + width={width} /> ); } diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index 5e48860a9b049..01655c0d7b2d7 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -17,12 +17,19 @@ import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overvie import { HasDataContextProvider } from './has_data_context'; import * as pluginContext from '../hooks/use_plugin_context'; import { PluginContextValue } from './plugin_context'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; const relativeStart = '2020-10-08T06:00:00.000Z'; const relativeEnd = '2020-10-08T07:00:00.000Z'; function wrapper({ children }: { children: React.ReactElement }) { - return {children}; + const history = createMemoryHistory(); + return ( + + {children} + + ); } function unregisterAll() { diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 085b7fd7ba028..a2628d37828a4 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -7,6 +7,7 @@ import { uniqueId } from 'lodash'; import React, { createContext, useEffect, useState } from 'react'; +import { useRouteMatch } from 'react-router-dom'; import { Alert } from '../../../alerting/common'; import { getDataHandler } from '../data_handler'; import { FETCH_STATUS } from '../hooks/use_fetcher'; @@ -41,35 +42,38 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode const [hasData, setHasData] = useState({}); + const isExploratoryView = useRouteMatch('/exploratory-view'); + useEffect( () => { - apps.forEach(async (app) => { - try { - if (app !== 'alert') { - const params = - app === 'ux' - ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } - : undefined; - - const result = await getDataHandler(app)?.hasData(params); + if (!isExploratoryView) + apps.forEach(async (app) => { + try { + if (app !== 'alert') { + const params = + app === 'ux' + ? { absoluteTime: { start: absoluteStart, end: absoluteEnd } } + : undefined; + + const result = await getDataHandler(app)?.hasData(params); + setHasData((prevState) => ({ + ...prevState, + [app]: { + hasData: result, + status: FETCH_STATUS.SUCCESS, + }, + })); + } + } catch (e) { setHasData((prevState) => ({ ...prevState, [app]: { - hasData: result, - status: FETCH_STATUS.SUCCESS, + hasData: undefined, + status: FETCH_STATUS.FAILURE, }, })); } - } catch (e) { - setHasData((prevState) => ({ - ...prevState, - [app]: { - hasData: undefined, - status: FETCH_STATUS.FAILURE, - }, - })); - } - }); + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [] diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts new file mode 100644 index 0000000000000..a354ac8a07f05 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromeBreadcrumb } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { MouseEvent, useEffect } from 'react'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { stringify } from 'query-string'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useQueryParams } from './use_query_params'; + +const EMPTY_QUERY = '?'; + +function handleBreadcrumbClick( + breadcrumbs: ChromeBreadcrumb[], + navigateToHref?: (url: string) => Promise +) { + return breadcrumbs.map((bc) => ({ + ...bc, + ...(bc.href + ? { + onClick: (event: MouseEvent) => { + if (navigateToHref && bc.href) { + event.preventDefault(); + navigateToHref(bc.href); + } + }, + } + : {}), + })); +} + +export const makeBaseBreadcrumb = (href: string, params?: any): EuiBreadcrumb => { + if (params) { + const crumbParams = { ...params }; + + delete crumbParams.statusFilter; + const query = stringify(crumbParams, { skipEmptyString: true, skipNull: true }); + href += query === EMPTY_QUERY ? '' : query; + } + return { + text: i18n.translate('xpack.observability.breadcrumbs.observability', { + defaultMessage: 'Observability', + }), + href, + }; +}; + +export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { + const params = useQueryParams(); + + const { + services: { chrome, application }, + } = useKibana(); + + const setBreadcrumbs = chrome?.setBreadcrumbs; + const appPath = application?.getUrlForApp('observability-overview') ?? ''; + const navigate = application?.navigateToUrl; + + useEffect(() => { + if (setBreadcrumbs) { + setBreadcrumbs( + handleBreadcrumbClick([makeBaseBreadcrumb(appPath, params)].concat(extraCrumbs), navigate) + ); + } + }, [appPath, extraCrumbs, navigate, params, setBreadcrumbs]); +}; diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx new file mode 100644 index 0000000000000..82a0fc39b8519 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; + +export function useQuickTimeRanges() { + const timePickerQuickRanges = useUiSetting( + UI_SETTINGS.TIMEPICKER_QUICK_RANGES + ); + + return timePickerQuickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); +} diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts index 25a12ab4a9ebd..e17f515ed6cb9 100644 --- a/x-pack/plugins/observability/public/hooks/use_values_list.ts +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -5,32 +5,58 @@ * 2.0. */ -import { IIndexPattern } from '../../../../../src/plugins/data/common'; +import { IndexPattern } from '../../../../../src/plugins/data/common'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { useFetcher } from './use_fetcher'; import { ESFilter } from '../../../../../typings/elasticsearch'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -interface Props { +export interface Props { sourceField: string; query?: string; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; filters?: ESFilter[]; + time?: { from: string; to: string }; } -export const useValuesList = ({ sourceField, indexPattern, query, filters }: Props) => { +export const useValuesList = ({ + sourceField, + indexPattern, + query = '', + filters, + time, +}: Props): { values: string[]; loading?: boolean } => { const { services: { data }, } = useKibana<{ data: DataPublicPluginStart }>(); - const { data: values, status } = useFetcher(() => { + const { from, to } = time ?? {}; + + const { data: values, loading } = useFetcher(() => { + if (!sourceField || !indexPattern) { + return []; + } return data.autocomplete.getValueSuggestions({ indexPattern, query: query || '', - field: indexPattern.fields.find(({ name }) => name === sourceField)!, - boolFilter: filters ?? [], + useTimeRange: !(from && to), + field: indexPattern.getFieldByName(sourceField)!, + boolFilter: + from && to + ? [ + ...(filters || []), + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ] + : filters || [], }); - }, [sourceField, query, data.autocomplete, indexPattern, filters]); + }, [query, sourceField, data.autocomplete, indexPattern, from, to, filters]); - return { values, loading: status === 'loading' || status === 'pending' }; + return { values: values as string[], loading }; }; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 35443ca090077..837404d273ee4 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -55,3 +55,4 @@ export * from './typings'; export { useChartTheme } from './hooks/use_chart_theme'; export { useTheme } from './hooks/use_theme'; export { getApmTraceUrl } from './utils/get_apm_trace_url'; +export { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils'; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 20817901dab82..49cc55832dcf2 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -14,6 +14,7 @@ import { OverviewPage } from '../pages/overview'; import { jsonRt } from './json_rt'; import { AlertsPage } from '../pages/alerts'; import { CasesPage } from '../pages/cases'; +import { ExploratoryViewPage } from '../components/shared/exploratory_view'; export type RouteParams = DecodeParams; @@ -115,4 +116,24 @@ export const routes = { }, ], }, + '/exploratory-view': { + handler: () => { + return ; + }, + params: { + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + refreshPaused: jsonRt.pipe(t.boolean), + refreshInterval: jsonRt.pipe(t.number), + }), + }, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.overview.exploratoryView', { + defaultMessage: 'Exploratory view', + }), + }, + ], + }, }; diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index f4e824784fe09..e9960833a1c4f 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -15,7 +15,7 @@ export interface Stat { export interface Coordinates { x: number; - y?: number; + y?: number | null; } export interface Series { diff --git a/x-pack/plugins/observability/public/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/utils/observability_index_patterns.ts new file mode 100644 index 0000000000000..b23a246105544 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/observability_index_patterns.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public'; + +export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum'; + +const indexPatternList: Record = { + synthetics: 'synthetics_static_index_pattern_id', + apm: 'apm_static_index_pattern_id', + rum: 'apm_static_index_pattern_id', + logs: 'logs_static_index_pattern_id', + metrics: 'metrics_static_index_pattern_id', +}; + +const appToPatternMap: Record = { + synthetics: 'heartbeat-*', + apm: 'apm-*', + rum: 'apm-*', + logs: 'logs-*,filebeat-*', + metrics: 'metrics-*,metricbeat-*', +}; + +export class ObservabilityIndexPatterns { + data?: DataPublicPluginStart; + + constructor(data: DataPublicPluginStart) { + this.data = data; + } + + async createIndexPattern(app: DataType) { + if (!this.data) { + throw new Error('data is not defined'); + } + + const pattern = appToPatternMap[app]; + + const fields = await this.data.indexPatterns.getFieldsForWildcard({ + pattern, + }); + + return await this.data.indexPatterns.createAndSave({ + fields, + title: pattern, + id: indexPatternList[app], + timeFieldName: '@timestamp', + }); + } + + async getIndexPattern(app: DataType): Promise { + if (!this.data) { + throw new Error('data is not defined'); + } + try { + return await this.data?.indexPatterns.get(indexPatternList[app]); + } catch (e) { + return await this.createIndexPattern(app || 'apm'); + } + } +} diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 6833948b86b18..f55ae640a8026 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -7,7 +7,14 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "server/**/*", "typings/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "public/**/*.json", + "server/**/*", + "typings/**/*", + "../../../typings/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts index 567990aca0537..d07af8c878c51 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { @@ -36,6 +37,7 @@ export type FactoryQueryTypes = OsqueryQueries; export interface RequestBasicOptions extends IEsSearchRequest { filterQuery: ESQuery | string | undefined; + aggregations?: Record; docValueFields?: DocValueFields[]; factoryQueryType?: FactoryQueryTypes; } diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx index 1880cec0ae8e2..660b837da6d93 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -22,6 +22,7 @@ import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction, ResultEdges } from '../../common/search_strategy'; import { useRouterNavigate } from '../common/lib/kibana'; +import { useOsqueryPolicies } from '../agents/use_osquery_policies'; const DataContext = createContext([]); @@ -91,12 +92,8 @@ const ActionResultsTableComponent: React.FC = ({ action setVisibleColumns, ]); - const { data: agentsData } = useAllAgents({ - activePage: 0, - limit: 1000, - direction: Direction.desc, - sortField: 'updated_at', - }); + const osqueryPolicyData = useOsqueryPolicies(); + const { agents } = useAllAgents(osqueryPolicyData); const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( () => ({ rowIndex, columnId }) => { @@ -134,8 +131,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent_status') { const agentIdValue = value.fields?.agent_id[0]; - // @ts-expect-error update types - const agent = find(['_id', agentIdValue], agentsData?.agents); + const agent = find(['_id', agentIdValue], agents); const online = agent?.active; const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; @@ -144,8 +140,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent') { const agentIdValue = value.fields?.agent_id[0]; - // @ts-expect-error update types - const agent = find(['_id', agentIdValue], agentsData?.agents); + const agent = find(['_id', agentIdValue], agents); const agentName = agent?.local_metadata.host.name; // eslint-disable-next-line react-hooks/rules-of-hooks @@ -162,8 +157,7 @@ const ActionResultsTableComponent: React.FC = ({ action return '-'; }, - // @ts-expect-error update types - [actionId, agentsData?.agents, pagination.pageIndex, pagination.pageSize] + [actionId, agents, pagination.pageIndex, pagination.pageSize] ); const tableSorting: EuiDataGridSorting = useMemo( diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index e41b74c672e9b..5f1b6a0d2f0b1 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -5,158 +5,222 @@ * 2.0. */ -import { find } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import { - EuiBasicTable, - EuiBasicTableColumn, - EuiBasicTableProps, - EuiTableSelectionType, - EuiHealth, -} from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@elastic/eui'; import { useAllAgents } from './use_all_agents'; -import { Direction } from '../../common/search_strategy'; +import { useAgentGroups } from './use_agent_groups'; +import { useOsqueryPolicies } from './use_osquery_policies'; import { Agent } from '../../common/shared_imports'; +import { + getNumAgentsInGrouping, + generateAgentCheck, + getNumOverlapped, + generateColorPicker, +} from './helpers'; -interface AgentsTableProps { - selectedAgents: string[]; - onChange: (payload: string[]) => void; +import { + ALL_AGENTS_LABEL, + AGENT_PLATFORMS_LABEL, + AGENT_POLICY_LABEL, + SELECT_AGENT_LABEL, + AGENT_SELECTION_LABEL, + generateSelectedAgentsMessage, +} from './translations'; + +import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types'; + +export interface AgentsSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; } -const AgentsTableComponent: React.FC = ({ selectedAgents, onChange }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('upgraded_at'); - const [sortDirection, setSortDirection] = useState(Direction.asc); - const [selectedItems, setSelectedItems] = useState([]); - const tableRef = useRef>(null); - - const onTableChange: EuiBasicTableProps['onChange'] = useCallback( - ({ page = {}, sort = {} }) => { - const { index: newPageIndex, size: newPageSize } = page; - - const { field: newSortField, direction: newSortDirection } = sort; +interface AgentsTableProps { + agentSelection: AgentsSelection; + onChange: (payload: AgentsSelection) => void; +} - setPageIndex(newPageIndex); - setPageSize(newPageSize); - setSortField(newSortField); - setSortDirection(newSortDirection); - }, - [] - ); +type GroupOption = EuiComboBoxOptionOption; - const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( - (newSelectedItems) => { - setSelectedItems(newSelectedItems); +const getColor = generateColorPicker(); - if (onChange) { - // @ts-expect-error update types - onChange(newSelectedItems.map((item) => item._id)); - } - }, - [onChange] +const AgentsTableComponent: React.FC = ({ onChange }) => { + const osqueryPolicyData = useOsqueryPolicies(); + const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups( + osqueryPolicyData ); + const { agents } = useAllAgents(osqueryPolicyData); + const [loading, setLoading] = useState(true); + const [options, setOptions] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + const [numAgentsSelected, setNumAgentsSelected] = useState(0); - const renderStatus = (online: string) => { - const color = online ? 'success' : 'danger'; - const label = online ? 'Online' : 'Offline'; - return {label}; - }; - - const { data = {} } = useAllAgents({ - activePage: pageIndex, - limit: pageSize, - direction: sortDirection, - sortField, - }); - - const columns: Array> = useMemo( - () => [ - { - field: 'local_metadata.elastic.agent.id', - name: 'id', - sortable: true, - truncateText: true, - }, - { - field: 'local_metadata.host.name', - name: 'hostname', - truncateText: true, - }, - + useEffect(() => { + const allAgentsLabel = ALL_AGENTS_LABEL; + const opts: GroupOption[] = [ { - field: 'active', - name: 'Online', - dataType: 'boolean', - render: (active: string) => renderStatus(active), - }, - ], - [] - ); - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - // @ts-expect-error update types - totalItemCount: data.totalCount ?? 0, - pageSizeOptions: [3, 5, 8], - }), - // @ts-expect-error update types - [pageIndex, pageSize, data.totalCount] - ); - - const sorting = useMemo( - () => ({ - sort: { - field: sortField, - direction: sortDirection, + label: allAgentsLabel, + options: [ + { + label: allAgentsLabel, + value: { groupType: AGENT_GROUP_KEY.All, size: totalNumAgents }, + color: getColor(AGENT_GROUP_KEY.All), + }, + ], }, - }), - [sortDirection, sortField] - ); + ]; + + if (groups.platforms.length > 0) { + const groupType = AGENT_GROUP_KEY.Platform; + opts.push({ + label: AGENT_PLATFORMS_LABEL, + options: groups.platforms.map(({ name, size }) => ({ + label: name, + color: getColor(groupType), + value: { groupType, size }, + })), + }); + } - const selection: EuiBasicTableProps['selection'] = useMemo( - () => ({ - selectable: (agent: Agent) => agent.active, - selectableMessage: (selectable: boolean) => (!selectable ? 'User is currently offline' : ''), - onSelectionChange, - initialSelected: selectedItems, - }), - [onSelectionChange, selectedItems] - ); + if (groups.policies.length > 0) { + const groupType = AGENT_GROUP_KEY.Policy; + opts.push({ + label: AGENT_POLICY_LABEL, + options: groups.policies.map(({ name, size }) => ({ + label: name, + color: getColor(groupType), + value: { groupType, size }, + })), + }); + } - useEffect(() => { - if ( - selectedAgents?.length && - // @ts-expect-error update types - data.agents?.length && - selectedItems.length !== selectedAgents.length - ) { - tableRef?.current?.setSelection( - // @ts-expect-error update types - selectedAgents.map((agentId) => find({ _id: agentId }, data.agents)) - ); + if (agents && agents.length > 0) { + const groupType = AGENT_GROUP_KEY.Agent; + opts.push({ + label: AGENT_SELECTION_LABEL, + options: (agents as Agent[]).map((agent: Agent) => ({ + label: agent.local_metadata.host.hostname, + color: getColor(groupType), + value: { + groupType, + groups: { policy: agent.policy_id ?? '', platform: agent.local_metadata.os.platform }, + id: agent.local_metadata.elastic.agent.id, + online: agent.active, + }, + })), + }); } - // @ts-expect-error update types - }, [selectedAgents, data.agents, selectedItems.length]); + setLoading(false); + setOptions(opts); + }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents]); + + const onSelection = useCallback( + (selection: GroupOption[]) => { + // TODO?: optimize this by making it incremental + const newAgentSelection: AgentsSelection = { + agents: [], + allAgentsSelected: false, + platformsSelected: [], + policiesSelected: [], + }; + // parse through the selections to be able to determine how many are actually selected + const selectedAgents = []; + const selectedGroups: SelectedGroups = { + policy: {}, + platform: {}, + }; + + // TODO: clean this up, make it less awkward + for (const opt of selection) { + const groupType = opt.value?.groupType; + let value; + switch (groupType) { + case AGENT_GROUP_KEY.All: + newAgentSelection.allAgentsSelected = true; + break; + case AGENT_GROUP_KEY.Platform: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.platform[opt.label] = value.size; + } + newAgentSelection.platformsSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Policy: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.policy[opt.label] = value.size ?? 0; + } + newAgentSelection.policiesSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Agent: + value = opt.value as AgentOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to count how many agents are selected if they are all selected + selectedAgents.push(opt.value); + } + // TODO: fix this casting by updating the opt type to be a union + newAgentSelection.agents.push(value.id as string); + break; + default: + // this should never happen! + // eslint-disable-next-line no-console + console.error(`unknown group type ${groupType}`); + } + } + if (newAgentSelection.allAgentsSelected) { + setNumAgentsSelected(totalNumAgents); + } else { + const checkAgent = generateAgentCheck(selectedGroups); + setNumAgentsSelected( + // filter out all the agents counted by selected policies and platforms + selectedAgents.filter((a) => checkAgent(a as AgentOptionValue)).length + + // add the number of agents added via policy and platform groups + getNumAgentsInGrouping(selectedGroups) - + // subtract the number of agents double counted by policy/platform selections + getNumOverlapped(selectedGroups, groups.overlap) + ); + } + onChange(newAgentSelection); + setSelectedOptions(selection); + }, + [groups, onChange, totalNumAgents] + ); + const renderOption = useCallback((option, searchValue, contentClassName) => { + const { label, value } = option; + return value?.groupType === AGENT_GROUP_KEY.Agent ? ( + + + {label} + + + ) : ( + + {label} +   + ({value?.size}) + + ); + }, []); return ( - - ref={tableRef} - // @ts-expect-error update types - // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop - items={data.agents ?? []} - itemId="_id" - columns={columns} - pagination={pagination} - sorting={sorting} - isSelectable={true} - selection={selection} - onChange={onTableChange} - rowHeader="firstName" - /> +
+

{SELECT_AGENT_LABEL}

+ {numAgentsSelected > 0 ? {generateSelectedAgentsMessage(numAgentsSelected)} : ''} +   + +
); }; diff --git a/x-pack/plugins/osquery/public/agents/helpers.test.ts b/x-pack/plugins/osquery/public/agents/helpers.test.ts new file mode 100644 index 0000000000000..3efd1b877d1a0 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/helpers.test.ts @@ -0,0 +1,222 @@ +/* + * Copyright 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 { getNumOverlapped, getNumAgentsInGrouping, processAggregations } from './helpers'; +import { Overlap, SelectedGroups } from './types'; + +describe('processAggregations', () => { + it('should handle empty inputs properly', () => { + const input = {}; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([]); + expect(policies).toEqual([]); + expect(overlap).toEqual({}); + }); + it('should handle platforms with no policies', () => { + const input = { + platforms: { + buckets: [ + { + key: 'darwin', + doc_count: 200, + policies: { + buckets: [], + }, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([ + { + name: 'darwin', + size: 200, + }, + ]); + expect(policies).toEqual([]); + expect(overlap).toEqual({}); + }); + it('should handle policies with no platforms', () => { + const input = { + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([]); + expect(policies).toEqual([ + { + name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + { + name: '8cd06880-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + ]); + expect(overlap).toEqual({}); + }); + it('should parse aggregation responses down into metadata objects', () => { + const input = { + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + platforms: { + buckets: [ + { + key: 'darwin', + doc_count: 200, + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([ + { + name: 'darwin', + size: 200, + }, + ]); + expect(policies).toEqual([ + { + name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + { + name: '8cd06880-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + ]); + expect(overlap).toEqual({ + darwin: { + '8cd06880-8a74-11eb-86cb-c58693443a4f': 100, + '8cd01a60-8a74-11eb-86cb-c58693443a4f': 100, + }, + }); + }); +}); + +describe('getNumAgentsInGrouping', () => { + it('should handle empty objects', () => { + const selectedGroups: SelectedGroups = {}; + expect(getNumAgentsInGrouping(selectedGroups)).toEqual(0); + }); + + it('should add up the quantities for the selected groups', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + }, + policy: { + policy_id1: 40, + }, + }; + expect(getNumAgentsInGrouping(selectedGroups)).toEqual(75); + }); +}); + +describe('getNumOverlapped', () => { + const overlap: Overlap = { + darwin: { + policy_id1: 15, + policy_id2: 35, + }, + linux: { + policy_id1: 25, + policy_id2: 10, + }, + }; + + it('should add up the quantities associated with a platform/policy selection', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + }, + policy: { + policy_id1: 40, + }, + }; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(25); + }); + + it('should gracefully handle empty objects', () => { + const selectedGroups: SelectedGroups = {}; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing platforms', () => { + const selectedGroups: SelectedGroups = { + policy: { + policy_id1: 40, + policy_id3: 40, + }, + }; + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing policies', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + windows: 40, + }, + }; + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing group selections', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + windows: 40, + }, + policy: { + policy_id1: 40, + policy_id3: 40, + }, + }; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(25); + }); +}); diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index fef17aadb62be..830fca5f57caa 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -5,15 +5,97 @@ * 2.0. */ +import { Aggregate, TermsAggregate } from '@elastic/elasticsearch/api/types'; +import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { PaginationInputPaginated, FactoryQueryTypes, StrategyResponseType, Inspect, } from '../../common/search_strategy'; +import { + AGENT_GROUP_KEY, + SelectedGroups, + Overlap, + Group, + AgentOptionValue, + AggregationDataPoint, +} from './types'; export type InspectResponse = Inspect & { response: string[] }; +export const getNumOverlapped = ( + { policy = {}, platform = {} }: SelectedGroups, + overlap: Overlap +) => { + let sum = 0; + Object.keys(platform).forEach((plat) => { + const policies = overlap[plat] ?? {}; + Object.keys(policy).forEach((pol) => { + sum += policies[pol] ?? 0; + }); + }); + return sum; +}; +export const processAggregations = (aggs: Record) => { + const platforms: Group[] = []; + const overlap: Overlap = {}; + const platformTerms = aggs.platforms as TermsAggregate; + const policyTerms = aggs.policies as TermsAggregate; + + const policies = policyTerms?.buckets.map((o) => ({ name: o.key, size: o.doc_count })) ?? []; + + if (platformTerms?.buckets) { + for (const { key, doc_count: size, policies: platformPolicies } of platformTerms.buckets) { + platforms.push({ name: key, size }); + if (platformPolicies?.buckets && policies.length > 0) { + overlap[key] = platformPolicies.buckets.reduce((acc: { [key: string]: number }, pol) => { + acc[pol.key] = pol.doc_count; + return acc; + }, {} as { [key: string]: number }); + } + } + } + + return { + platforms, + overlap, + policies, + }; +}; +export const generateColorPicker = () => { + const visColorsBehindText = euiPaletteColorBlindBehindText(); + const typeColors = new Map(); + return (type: AGENT_GROUP_KEY) => { + if (!typeColors.has(type)) { + typeColors.set(type, visColorsBehindText[typeColors.size]); + } + return typeColors.get(type); + }; +}; + +export const getNumAgentsInGrouping = (selectedGroups: SelectedGroups) => { + let sum = 0; + Object.keys(selectedGroups).forEach((g) => { + const group = selectedGroups[g]; + sum += Object.keys(group).reduce((acc, k) => acc + group[k], 0); + }); + return sum; +}; + +export const generateAgentCheck = (selectedGroups: SelectedGroups) => { + return ({ groups }: AgentOptionValue) => { + return Object.keys(groups) + .map((group) => { + const selectedGroup = selectedGroups[group]; + const agentGroup = groups[group]; + // check if the agent platform/policy is selected + return selectedGroup[agentGroup]; + }) + .every((a) => !a); + }; +}; + export const generateTablePaginationOptions = ( activePage: number, limit: number, diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 0d9d9a8a12b8f..af99a73d63de2 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -7,6 +7,42 @@ import { i18n } from '@kbn/i18n'; +export const generateSelectedAgentsMessage = (numAgents: number): string => { + if (numAgents === 0) { + return ''; + } else if (numAgents === 1) { + return i18n.translate('xpack.osquery.agents.oneSelectedAgentText', { + defaultMessage: '{numAgents} agent selected.', + values: { numAgents }, + }); + } else { + return i18n.translate('xpack.osquery.agents.mulitpleSelectedAgentsText', { + defaultMessage: '{numAgents} agents selected.', + values: { numAgents }, + }); + } +}; + +export const ALL_AGENTS_LABEL = i18n.translate('xpack.osquery.agents.allAgentsLabel', { + defaultMessage: `All agents`, +}); + +export const AGENT_PLATFORMS_LABEL = i18n.translate('xpack.osquery.agents.platformLabel', { + defaultMessage: `Platform`, +}); + +export const AGENT_POLICY_LABEL = i18n.translate('xpack.osquery.agents.policyLabel', { + defaultMessage: `Policy`, +}); + +export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.selectionLabel', { + defaultMessage: `Agents`, +}); + +export const SELECT_AGENT_LABEL = i18n.translate('xpack.osquery.agents.selectAgentLabel', { + defaultMessage: `Select Agents`, +}); + export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { defaultMessage: `An error has occurred on all agents search`, }); diff --git a/x-pack/plugins/osquery/public/agents/types.ts b/x-pack/plugins/osquery/public/agents/types.ts new file mode 100644 index 0000000000000..2fa8ddaf345cd --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/types.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 { TermsAggregate } from '@elastic/elasticsearch/api/types'; + +interface BaseDataPoint { + key: string; + doc_count: number; +} + +export type AggregationDataPoint = BaseDataPoint & { + [key: string]: TermsAggregate; +}; + +export interface Group { + name: string; + size: number; +} +export interface Overlap { + [platform: string]: { [policy: string]: number }; +} + +export interface SelectedGroups { + [groupType: string]: { [groupName: string]: number }; +} + +interface BaseGroupOption { + groupType: AGENT_GROUP_KEY; +} + +export type AgentOptionValue = BaseGroupOption & { + groups: { [groupType: string]: string }; + online: boolean; + id: string; +}; + +export type GroupOptionValue = BaseGroupOption & { + size: number; +}; + +export enum AGENT_GROUP_KEY { + All, + Platform, + Policy, + Agent, +} diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts new file mode 100644 index 0000000000000..0eaca65d02d4b --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { useKibana } from '../common/lib/kibana'; + +import { + OsqueryQueries, + AgentsRequestOptions, + AgentsStrategyResponse, +} from '../../common/search_strategy'; + +import { generateTablePaginationOptions, processAggregations } from './helpers'; +import { Overlap, Group } from './types'; + +interface UseAgentGroups { + osqueryPolicies: string[]; + osqueryPoliciesLoading: boolean; +} + +export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => { + const { data } = useKibana().services; + + const [platforms, setPlatforms] = useState([]); + const [policies, setPolicies] = useState([]); + const [loading, setLoading] = useState(true); + const [overlap, setOverlap] = useState(() => ({})); + const [totalCount, setTotalCount] = useState(0); + useQuery( + ['agentGroups'], + async () => { + const responseData = await data.search + .search( + { + filterQuery: { terms: { policy_id: osqueryPolicies } }, + factoryQueryType: OsqueryQueries.agents, + aggregations: { + platforms: { + terms: { + field: 'local_metadata.os.platform', + }, + aggs: { + policies: { + terms: { + field: 'policy_id', + }, + }, + }, + }, + policies: { + terms: { + field: 'policy_id', + }, + }, + }, + pagination: generateTablePaginationOptions(0, 9000), + sort: { + direction: 'asc', + field: 'local_metadata.os.platform', + }, + } as AgentsRequestOptions, + { + strategy: 'osquerySearchStrategy', + } + ) + .toPromise(); + + if (responseData.rawResponse.aggregations) { + const { + platforms: newPlatforms, + overlap: newOverlap, + policies: newPolicies, + } = processAggregations(responseData.rawResponse.aggregations); + + setPlatforms(newPlatforms); + setOverlap(newOverlap); + setPolicies(newPolicies); + } + + setLoading(false); + setTotalCount(responseData.totalCount); + }, + { + enabled: !osqueryPoliciesLoading, + } + ); + + return { + loading, + totalCount, + groups: { + platforms, + policies, + overlap, + }, + }; +}; diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 663c6936fe55b..607f9ae007692 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -5,94 +5,32 @@ * 2.0. */ -import deepEqual from 'fast-deep-equal'; -import { useEffect, useState } from 'react'; import { useQuery } from 'react-query'; -import { createFilter } from '../common/helpers'; import { useKibana } from '../common/lib/kibana'; -import { - PageInfoPaginated, - OsqueryQueries, - AgentsRequestOptions, - AgentsStrategyResponse, - Direction, -} from '../../common/search_strategy'; -import { ESTermQuery } from '../../common/typed_json'; -import { Agent } from '../../common/shared_imports'; - -import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; - -export interface AgentsArgs { - agents: Agent[]; - id: string; - inspect: InspectResponse; - isInspected: boolean; - pageInfo: PageInfoPaginated; - totalCount: number; -} interface UseAllAgents { - activePage: number; - direction: Direction; - limit: number; - sortField: string; - filterQuery?: ESTermQuery | string; - skip?: boolean; + osqueryPolicies: string[]; + osqueryPoliciesLoading: boolean; } -export const useAllAgents = ({ - activePage, - direction, - limit, - sortField, - filterQuery, - skip = false, -}: UseAllAgents) => { - const { data } = useKibana().services; - - const [agentsRequest, setHostRequest] = useState(null); - - const response = useQuery( - ['agents', { activePage, direction, limit, sortField }], +export const useAllAgents = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents) => { + // TODO: properly fetch these in an async manner + const { http } = useKibana().services; + const { isLoading: agentsLoading, data: agentData } = useQuery( + ['agents', osqueryPolicies], async () => { - if (!agentsRequest) return Promise.resolve(); - - const responseData = await data.search - .search(agentsRequest, { - strategy: 'osquerySearchStrategy', - }) - .toPromise(); - - return { - ...responseData, - agents: responseData.edges, - inspect: getInspectResponse(responseData), - }; + return await http.get('/api/fleet/agents', { + query: { + kuery: osqueryPolicies.map((p) => `policy_id:${p}`).join(' or '), + perPage: 9000, + }, + }); }, { - enabled: !skip && !!agentsRequest, + enabled: !osqueryPoliciesLoading, } ); - useEffect(() => { - setHostRequest((prevRequest) => { - const myRequest = { - ...(prevRequest ?? {}), - factoryQueryType: OsqueryQueries.agents, - filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), - sort: { - direction, - field: sortField, - }, - }; - if (!deepEqual(prevRequest, myRequest)) { - return myRequest; - } - return prevRequest; - }); - }, [activePage, direction, filterQuery, limit, sortField]); - - return response; + return { agentsLoading, agents: agentData?.list }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts new file mode 100644 index 0000000000000..f786e9167d2f8 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; +import { useKibana } from '../common/lib/kibana'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; + +export const useOsqueryPolicies = () => { + const { http } = useKibana().services; + + const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies } = useQuery( + ['osqueryPolicies'], + async () => { + return await http.get('/api/fleet/package_policies', { + query: { + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:osquery_manager`, + }, + }); + }, + { select: (data) => data.items.map((p: { policy_id: string }) => p.policy_id) } + ); + + return { osqueryPoliciesLoading, osqueryPolicies }; +}; diff --git a/x-pack/plugins/osquery/public/editor/osquery_schema/v4.7.0.json b/x-pack/plugins/osquery/public/editor/osquery_schema/v4.7.0.json new file mode 100644 index 0000000000000..eac29577051a4 --- /dev/null +++ b/x-pack/plugins/osquery/public/editor/osquery_schema/v4.7.0.json @@ -0,0 +1 @@ +[{"name":"account_policy_data"},{"name":"acpi_tables"},{"name":"ad_config"},{"name":"alf"},{"name":"alf_exceptions"},{"name":"alf_explicit_auths"},{"name":"app_schemes"},{"name":"apparmor_events"},{"name":"apparmor_profiles"},{"name":"appcompat_shims"},{"name":"apps"},{"name":"apt_sources"},{"name":"arp_cache"},{"name":"asl"},{"name":"atom_packages"},{"name":"augeas"},{"name":"authenticode"},{"name":"authorization_mechanisms"},{"name":"authorizations"},{"name":"authorized_keys"},{"name":"autoexec"},{"name":"azure_instance_metadata"},{"name":"azure_instance_tags"},{"name":"background_activities_moderator"},{"name":"battery"},{"name":"bitlocker_info"},{"name":"block_devices"},{"name":"bpf_process_events"},{"name":"bpf_socket_events"},{"name":"browser_plugins"},{"name":"carbon_black_info"},{"name":"carves"},{"name":"certificates"},{"name":"chassis_info"},{"name":"chocolatey_packages"},{"name":"chrome_extension_content_scripts"},{"name":"chrome_extensions"},{"name":"connectivity"},{"name":"cpu_info"},{"name":"cpu_time"},{"name":"cpuid"},{"name":"crashes"},{"name":"crontab"},{"name":"cups_destinations"},{"name":"cups_jobs"},{"name":"curl"},{"name":"curl_certificate"},{"name":"deb_packages"},{"name":"default_environment"},{"name":"device_file"},{"name":"device_firmware"},{"name":"device_hash"},{"name":"device_partitions"},{"name":"disk_encryption"},{"name":"disk_events"},{"name":"disk_info"},{"name":"dns_cache"},{"name":"dns_resolvers"},{"name":"docker_container_fs_changes"},{"name":"docker_container_labels"},{"name":"docker_container_mounts"},{"name":"docker_container_networks"},{"name":"docker_container_ports"},{"name":"docker_container_processes"},{"name":"docker_container_stats"},{"name":"docker_containers"},{"name":"docker_image_history"},{"name":"docker_image_labels"},{"name":"docker_image_layers"},{"name":"docker_images"},{"name":"docker_info"},{"name":"docker_network_labels"},{"name":"docker_networks"},{"name":"docker_version"},{"name":"docker_volume_labels"},{"name":"docker_volumes"},{"name":"drivers"},{"name":"ec2_instance_metadata"},{"name":"ec2_instance_tags"},{"name":"elf_dynamic"},{"name":"elf_info"},{"name":"elf_sections"},{"name":"elf_segments"},{"name":"elf_symbols"},{"name":"etc_hosts"},{"name":"etc_protocols"},{"name":"etc_services"},{"name":"event_taps"},{"name":"example"},{"name":"extended_attributes"},{"name":"fan_speed_sensors"},{"name":"fbsd_kmods"},{"name":"file"},{"name":"file_events"},{"name":"firefox_addons"},{"name":"gatekeeper"},{"name":"gatekeeper_approved_apps"},{"name":"groups"},{"name":"hardware_events"},{"name":"hash"},{"name":"homebrew_packages"},{"name":"hvci_status"},{"name":"ibridge_info"},{"name":"ie_extensions"},{"name":"intel_me_info"},{"name":"interface_addresses"},{"name":"interface_details"},{"name":"interface_ipv6"},{"name":"iokit_devicetree"},{"name":"iokit_registry"},{"name":"iptables"},{"name":"kernel_extensions"},{"name":"kernel_info"},{"name":"kernel_modules"},{"name":"kernel_panics"},{"name":"keychain_acls"},{"name":"keychain_items"},{"name":"known_hosts"},{"name":"kva_speculative_info"},{"name":"last"},{"name":"launchd"},{"name":"launchd_overrides"},{"name":"listening_ports"},{"name":"lldp_neighbors"},{"name":"load_average"},{"name":"location_services"},{"name":"logged_in_users"},{"name":"logical_drives"},{"name":"logon_sessions"},{"name":"lxd_certificates"},{"name":"lxd_cluster"},{"name":"lxd_cluster_members"},{"name":"lxd_images"},{"name":"lxd_instance_config"},{"name":"lxd_instance_devices"},{"name":"lxd_instances"},{"name":"lxd_networks"},{"name":"lxd_storage_pools"},{"name":"magic"},{"name":"managed_policies"},{"name":"md_devices"},{"name":"md_drives"},{"name":"md_personalities"},{"name":"mdfind"},{"name":"mdls"},{"name":"memory_array_mapped_addresses"},{"name":"memory_arrays"},{"name":"memory_device_mapped_addresses"},{"name":"memory_devices"},{"name":"memory_error_info"},{"name":"memory_info"},{"name":"memory_map"},{"name":"mounts"},{"name":"msr"},{"name":"nfs_shares"},{"name":"npm_packages"},{"name":"ntdomains"},{"name":"ntfs_acl_permissions"},{"name":"ntfs_journal_events"},{"name":"nvram"},{"name":"oem_strings"},{"name":"office_mru"},{"name":"os_version"},{"name":"osquery_events"},{"name":"osquery_extensions"},{"name":"osquery_flags"},{"name":"osquery_info"},{"name":"osquery_packs"},{"name":"osquery_registry"},{"name":"osquery_schedule"},{"name":"package_bom"},{"name":"package_install_history"},{"name":"package_receipts"},{"name":"patches"},{"name":"pci_devices"},{"name":"physical_disk_performance"},{"name":"pipes"},{"name":"pkg_packages"},{"name":"platform_info"},{"name":"plist"},{"name":"portage_keywords"},{"name":"portage_packages"},{"name":"portage_use"},{"name":"power_sensors"},{"name":"powershell_events"},{"name":"preferences"},{"name":"process_envs"},{"name":"process_events"},{"name":"process_file_events"},{"name":"process_memory_map"},{"name":"process_namespaces"},{"name":"process_open_files"},{"name":"process_open_pipes"},{"name":"process_open_sockets"},{"name":"processes"},{"name":"programs"},{"name":"prometheus_metrics"},{"name":"python_packages"},{"name":"quicklook_cache"},{"name":"registry"},{"name":"routes"},{"name":"rpm_package_files"},{"name":"rpm_packages"},{"name":"running_apps"},{"name":"safari_extensions"},{"name":"sandboxes"},{"name":"scheduled_tasks"},{"name":"screenlock"},{"name":"selinux_events"},{"name":"selinux_settings"},{"name":"services"},{"name":"shadow"},{"name":"shared_folders"},{"name":"shared_memory"},{"name":"shared_resources"},{"name":"sharing_preferences"},{"name":"shell_history"},{"name":"shellbags"},{"name":"shimcache"},{"name":"signature"},{"name":"sip_config"},{"name":"smart_drive_info"},{"name":"smbios_tables"},{"name":"smc_keys"},{"name":"socket_events"},{"name":"ssh_configs"},{"name":"startup_items"},{"name":"sudoers"},{"name":"suid_bin"},{"name":"syslog_events"},{"name":"system_controls"},{"name":"system_extensions"},{"name":"system_info"},{"name":"systemd_units"},{"name":"temperature_sensors"},{"name":"time"},{"name":"time_machine_backups"},{"name":"time_machine_destinations"},{"name":"ulimit_info"},{"name":"uptime"},{"name":"usb_devices"},{"name":"user_events"},{"name":"user_groups"},{"name":"user_interaction_events"},{"name":"user_ssh_keys"},{"name":"userassist"},{"name":"users"},{"name":"video_info"},{"name":"virtual_memory_info"},{"name":"wifi_networks"},{"name":"wifi_status"},{"name":"wifi_survey"},{"name":"winbaseobj"},{"name":"windows_crashes"},{"name":"windows_eventlog"},{"name":"windows_events"},{"name":"windows_optional_features"},{"name":"windows_security_center"},{"name":"windows_security_products"},{"name":"wmi_bios_info"},{"name":"wmi_cli_event_consumers"},{"name":"wmi_event_filters"},{"name":"wmi_filter_consumer_binding"},{"name":"wmi_script_event_consumers"},{"name":"xprotect_entries"},{"name":"xprotect_meta"},{"name":"xprotect_reports"},{"name":"yara"},{"name":"yara_events"},{"name":"ycloud_instance_metadata"},{"name":"yum_sources"}] \ No newline at end of file diff --git a/x-pack/plugins/osquery/public/editor/osquery_tables.ts b/x-pack/plugins/osquery/public/editor/osquery_tables.ts index 8fdacbdf3574e..d114cda742f9d 100644 --- a/x-pack/plugins/osquery/public/editor/osquery_tables.ts +++ b/x-pack/plugins/osquery/public/editor/osquery_tables.ts @@ -20,7 +20,7 @@ let osqueryTables: TablesJSON | null = null; export const getOsqueryTables = () => { if (!osqueryTables) { // eslint-disable-next-line @typescript-eslint/no-var-requires - osqueryTables = normalizeTables(require('./osquery_schema/v4.6.0.json')); + osqueryTables = normalizeTables(require('./osquery_schema/v4.7.0.json')); } return osqueryTables; }; diff --git a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx index 7a93b5d2491db..4bc9262af7613 100644 --- a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx @@ -7,10 +7,10 @@ import React, { useCallback } from 'react'; import { FieldHook } from '../../shared_imports'; -import { AgentsTable } from '../../agents/agents_table'; +import { AgentsTable, AgentsSelection } from '../../agents/agents_table'; interface AgentsTableFieldProps { - field: FieldHook; + field: FieldHook; } const AgentsTableFieldComponent: React.FC = ({ field }) => { @@ -24,7 +24,7 @@ const AgentsTableFieldComponent: React.FC = ({ field }) = [value, setValue] ); - return ; + return ; }; export const AgentsTableField = React.memo(AgentsTableFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/index.tsx b/x-pack/plugins/osquery/public/live_query/form/index.tsx index 4a69e2fc0e76d..7e19bee530ec5 100644 --- a/x-pack/plugins/osquery/public/live_query/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/index.tsx @@ -40,7 +40,7 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on return (
- + diff --git a/x-pack/plugins/osquery/scripts/schema_formatter/script.ts b/x-pack/plugins/osquery/scripts/schema_formatter/script.ts index 146bd4a9d49d7..578c4a1120962 100644 --- a/x-pack/plugins/osquery/scripts/schema_formatter/script.ts +++ b/x-pack/plugins/osquery/scripts/schema_formatter/script.ts @@ -15,7 +15,7 @@ interface DestField { run( async ({ flags }) => { - const schemaPath = path.resolve('../../public/editor/osquery_schema/'); + const schemaPath = path.resolve('./public/editor/osquery_schema/'); const schemaFile = path.join(schemaPath, flags.schema_version as string); const schemaData = await require(schemaFile); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts new file mode 100644 index 0000000000000..975770e594367 --- /dev/null +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.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 { ElasticsearchClient } from 'src/core/server'; +import { OsqueryAppContext } from './osquery_app_context_services'; + +export interface AgentSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; +} + +export const parseAgentSelection = async ( + esClient: ElasticsearchClient, + context: OsqueryAppContext, + agentSelection: AgentSelection +) => { + let selectedAgents: string[] = []; + const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection; + const agentService = context.service.getAgentService(); + if (agentService) { + if (allAgentsSelected) { + // TODO: actually fetch all the agents + const { agents: fetchedAgents } = await agentService.listAgents(esClient, { + perPage: 9000, + showInactive: true, + }); + selectedAgents.push(...fetchedAgents.map((a) => a.id)); + } else { + if (platformsSelected.length > 0 || policiesSelected.length > 0) { + const kueryFragments = []; + if (platformsSelected.length) { + kueryFragments.push( + ...platformsSelected.map((platform) => `local_metadata.os.platform:${platform}`) + ); + } + if (policiesSelected.length) { + kueryFragments.push(...policiesSelected.map((policy) => `policy_id:${policy}`)); + } + const kuery = kueryFragments.join(' or '); + // TODO: actually fetch all the agents + const { agents: fetchedAgents } = await agentService.listAgents(esClient, { + kuery, + perPage: 9000, + showInactive: true, + }); + selectedAgents.push(...fetchedAgents.map((a) => a.id)); + } + selectedAgents.push(...agents); + selectedAgents = Array.from(new Set(selectedAgents)); + } + } + return selectedAgents; +}; diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index 25212bc3bf5cc..7068243cc0fb7 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -12,8 +12,11 @@ import moment from 'moment'; import { IRouter } from '../../../../../../src/core/server'; import { packSavedObjectType, savedQuerySavedObjectType } from '../../../common/types'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const createActionRoute = (router: IRouter) => { +import { parseAgentSelection, AgentSelection } from '../../lib/parse_agent_groups'; + +export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( { path: '/internal/osquery/action', @@ -24,7 +27,8 @@ export const createActionRoute = (router: IRouter) => { }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asInternalUser; - + const { agentSelection } = request.body as { agentSelection: AgentSelection }; + const selectedAgents = await parseAgentSelection(esClient, osqueryContext, agentSelection); // @ts-expect-error update validation if (request.body.pack_id) { const savedObjectsClient = context.core.savedObjects.client; @@ -72,8 +76,7 @@ export const createActionRoute = (router: IRouter) => { expiration: moment().add(2, 'days').toISOString(), type: 'INPUT_ACTION', input_type: 'osquery', - // @ts-expect-error update validation - agents: request.body.agents, + agents: selectedAgents, data: { id: query.id, // @ts-expect-error update validation @@ -103,8 +106,7 @@ export const createActionRoute = (router: IRouter) => { expiration: moment().add(2, 'days').toISOString(), type: 'INPUT_ACTION', input_type: 'osquery', - // @ts-expect-error update validation - agents: request.body.agents, + agents: selectedAgents, data: { // @ts-expect-error update validation id: request.body.query.id ?? uuid.v4(), diff --git a/x-pack/plugins/osquery/server/routes/action/index.ts b/x-pack/plugins/osquery/server/routes/action/index.ts index 37e04fac5b986..fcf89d79dd0ee 100644 --- a/x-pack/plugins/osquery/server/routes/action/index.ts +++ b/x-pack/plugins/osquery/server/routes/action/index.ts @@ -7,7 +7,8 @@ import { IRouter } from '../../../../../../src/core/server'; import { createActionRoute } from './create_action_route'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const initActionRoutes = (router: IRouter) => { - createActionRoute(router); +export const initActionRoutes = (router: IRouter, context: OsqueryAppContext) => { + createActionRoute(router, context); }; diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts index 29df227583992..59d4085a77be1 100644 --- a/x-pack/plugins/osquery/server/routes/index.ts +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -13,7 +13,7 @@ import { OsqueryAppContext } from '../lib/osquery_app_context_services'; import { initPackRoutes } from './pack'; export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => { - initActionRoutes(router); + initActionRoutes(router, context); initPackRoutes(router); initSavedQueryRoutes(router); initScheduledQueryRoutes(router, context); diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts index 4ad6022017966..52101462270c7 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts @@ -7,15 +7,18 @@ import { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; import { AgentsRequestOptions } from '../../../../../common/search_strategy'; -// import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; export const buildAgentsQuery = ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars filterQuery, pagination: { cursorStart, querySize }, sort, + aggregations, }: AgentsRequestOptions): ISearchRequestParams => { - // const filter = [...createQueryFilterClauses(filterQuery)]; + const filter = [ + { term: { active: { value: 'true' } } }, + ...createQueryFilterClauses(filterQuery), + ]; const dslQuery = { allowNoIndices: true, @@ -23,12 +26,11 @@ export const buildAgentsQuery = ({ ignoreUnavailable: true, body: { query: { - term: { - active: { - value: 'true', - }, + bool: { + filter, }, }, + aggs: aggregations, track_total_hits: true, sort: [ { diff --git a/x-pack/plugins/reporting/server/config/index.test.ts b/x-pack/plugins/reporting/server/config/index.test.ts index d7c12937cda7c..a395cd23288eb 100644 --- a/x-pack/plugins/reporting/server/config/index.test.ts +++ b/x-pack/plugins/reporting/server/config/index.test.ts @@ -21,7 +21,7 @@ const applyReportingDeprecations = (settings: Record = {}) => { deprecation, path: CONFIG_PATH, })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index 06975aa85f1e4..4b97dbc1e2a84 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -24,12 +24,12 @@ export const config: PluginConfigDescriptor = { unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), unused('poll.jobsRefresh.intervalErrorMultiplier'), unused('kibanaApp'), - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const reporting = get(settings, fromPath); if (reporting?.index) { - log( - `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details` - ); + addDeprecation({ + message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + }); } return settings; }, diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx index 8433f54a73343..29d87e31797cc 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -262,6 +262,7 @@ export const UserForm: FunctionComponent = ({ > = ({ > = ({ > = ({ > = ({ > { const userMenuLinkMenuItems = userMenuLinks .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) .map(({ label, iconType, href }: UserMenuLink) => ({ - name: {label}, + name: label, icon: , href, 'data-test-subj': `userMenuLink__${label}`, diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 2b6ad603e6163..80173dd42a49e 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -20,7 +20,7 @@ const applyConfigDeprecations = (settings: Record = {}) => { deprecation, path: 'xpack.security', })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index a7bb5e09fb919..eae996fe2a5c0 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -22,16 +22,17 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ unused('authorization.legacyFallback.enabled'), unused('authc.saml.maxRedirectURLSize'), // Deprecation warning for the old array-based format of `xpack.security.authc.providers`. - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { if (Array.isArray(settings?.xpack?.security?.authc?.providers)) { - log( - 'Defining `xpack.security.authc.providers` as an array of provider types is deprecated. Use extended `object` format instead.' - ); + addDeprecation({ + message: + 'Defining `xpack.security.authc.providers` as an array of provider types is deprecated. Use extended `object` format instead.', + }); } return settings; }, - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const hasProviderType = (providerType: string) => { const providers = settings?.xpack?.security?.authc?.providers; if (Array.isArray(providers)) { @@ -44,31 +45,34 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ }; if (hasProviderType('basic') && hasProviderType('token')) { - log( - 'Enabling both `basic` and `token` authentication providers in `xpack.security.authc.providers` is deprecated. Login page will only use `token` provider.' - ); + addDeprecation({ + message: + 'Enabling both `basic` and `token` authentication providers in `xpack.security.authc.providers` is deprecated. Login page will only use `token` provider.', + }); } return settings; }, - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const samlProviders = (settings?.xpack?.security?.authc?.providers?.saml ?? {}) as Record< string, any >; if (Object.values(samlProviders).find((provider) => !!provider.maxRedirectURLSize)) { - log( - '`xpack.security.authc.providers.saml..maxRedirectURLSize` is deprecated and is no longer used' - ); + addDeprecation({ + message: + '`xpack.security.authc.providers.saml..maxRedirectURLSize` is deprecated and is no longer used', + }); } return settings; }, - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { if (settings?.xpack?.security?.enabled === false) { - log( - 'Disabling the security plugin (`xpack.security.enabled`) will not be supported in the next major version (8.0). ' + - 'To turn off security features, disable them in Elasticsearch instead.' - ); + addDeprecation({ + message: + 'Disabling the security plugin (`xpack.security.enabled`) will not be supported in the next major version (8.0). ' + + 'To turn off security features, disable them in Elasticsearch instead.', + }); } return settings; }, diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index e1e78f8e310e1..129d592edd264 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -502,7 +502,7 @@ describe('indicator match', () => { { line: 3, text: - ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', }, { line: 2, text: ' }' }, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index af90d17fe62b8..43d5c66655808 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -12,6 +12,8 @@ import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; import { useKibana } from '../../lib/kibana'; import { SourcererScopeName } from '../../store/sourcerer/model'; @@ -91,6 +93,8 @@ const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={endDate} id={timelineId} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 3ecc17589fe08..8962f5e6c5146 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -26,6 +26,8 @@ import { KqlMode } from '../../../timelines/store/timeline/model'; import { SortDirection } from '../../../timelines/components/timeline/body/sort'; import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; jest.mock('../../../timelines/components/graph_overlay', () => ({ @@ -99,6 +101,8 @@ const eventsViewerDefaultProps = { query: '', language: 'kql', }, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, sort: [ { @@ -118,6 +122,8 @@ describe('EventsViewer', () => { defaultModel: eventsDefaultModel, end: to, id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, start: from, scopeId: SourcererScopeName.timeline, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 050cd92b0556e..e6e868f1a7365 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; @@ -41,7 +40,9 @@ import { useManageTimeline } from '../../../timelines/components/manage_timeline import { ExitFullScreen } from '../exit_full_screen'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px @@ -122,6 +123,8 @@ interface Props { kqlMode: KqlMode; query: Query; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; start: string; sort: Sort[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; @@ -146,8 +149,10 @@ const EventsViewerComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, kqlMode, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, sort, utilityBar, @@ -310,6 +315,8 @@ const EventsViewerComponent: React.FC = ({ isEventViewer={true} onRuleChange={onRuleChange} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ @@ -343,6 +350,7 @@ const EventsViewerComponent: React.FC = ({ export const EventsViewer = React.memo( EventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && prevProps.columns === nextProps.columns && @@ -359,6 +367,8 @@ export const EventsViewer = React.memo( prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.start === nextProps.start && deepEqual(prevProps.sort, nextProps.sort) && prevProps.utilityBar === nextProps.utilityBar && diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 5004c23f9111c..cd27177643b44 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -18,7 +18,9 @@ import { StatefulEventsViewer } from '.'; import { eventsDefaultModel } from './default_model'; import { TimelineId } from '../../../../common/types/timeline'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { useTimelineEvents } from '../../../timelines/containers'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; jest.mock('../../../timelines/containers', () => ({ useTimelineEvents: jest.fn(), @@ -38,6 +40,8 @@ const testProps = { end: to, indexNames: [], id: TimelineId.test, + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, scopeId: SourcererScopeName.default, start: from, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 59dc756bb2b3e..b58aa2236d292 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -22,6 +22,8 @@ import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { DetailsPanel } from '../../../timelines/components/side_panel'; +import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; +import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; @@ -41,6 +43,8 @@ export interface OwnProps { headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; } @@ -67,8 +71,10 @@ const StatefulEventsViewerComponent: React.FC = ({ itemsPerPageOptions, kqlMode, pageFilters, - query, onRuleChange, + query, + renderCellValue, + rowRenderers, start, scopeId, showCheckboxes, @@ -129,6 +135,8 @@ const StatefulEventsViewerComponent: React.FC = ({ kqlMode={kqlMode} query={query} onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} start={start} sort={sort} utilityBar={utilityBar} @@ -201,6 +209,7 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && prevProps.scopeId === nextProps.scopeId && @@ -215,6 +224,8 @@ export const StatefulEventsViewer = connector( deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && prevProps.kqlMode === nextProps.kqlMode && deepEqual(prevProps.query, nextProps.query) && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && deepEqual(prevProps.sort, nextProps.sort) && prevProps.start === nextProps.start && deepEqual(prevProps.pageFilters, nextProps.pageFilters) && diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.test.ts index 5def34a33cd15..310502c04c1b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/host_to_influencers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { HostItem } from '../../../../graphql/types'; +import { HostItem } from '../../../../../common/search_strategy/security_solution/hosts'; import { InfluencerInput } from '../types'; import { hostToInfluencers } from './host_to_influencers'; @@ -28,6 +28,7 @@ describe('host_to_influencer', () => { test('returns a null if the host.name is null', () => { const hostItem: HostItem = { host: { + // @ts-expect-error name: null, }, }; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index c933afc98856b..eac8fb7f6813e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -204,6 +204,7 @@ export const mockGlobalState: State = { timelineById: { test: { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.notes, deletedEventIds: [], id: 'test', savedObjectId: null, 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 a9214eed60b36..5aef3b97c81b7 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 @@ -2062,6 +2062,7 @@ export const mockTimelineResults: OpenTimelineResult[] = [ export const mockTimelineModel: TimelineModel = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.notes, columns: [ { columnHeaderType: 'not-filtered', @@ -2209,6 +2210,7 @@ export const defaultTimelineProps: CreateTimelineProps = { from: '2018-11-05T18:58:25.937Z', timeline: { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', id: '@timestamp', type: 'number', width: 190 }, { columnHeaderType: 'not-filtered', id: 'message', width: 180 }, 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 a8aa42a3a59ff..6eccba954a175 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 @@ -108,6 +108,7 @@ describe('alert actions', () => { notes: null, timeline: { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', 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 6c88b8e29800b..cf6db52d0cece 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 @@ -48,6 +48,8 @@ import { import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { buildTimeRangeFilter } from './helpers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -336,6 +338,8 @@ export const AlertsTableComponent: React.FC = ({ headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.detections} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index bb18148195cc8..1df8716ba76e4 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -1637,7 +1637,18 @@ "isDeprecated": false, "deprecationReason": null }, - { "name": "warning", "description": "", "isDeprecated": false, "deprecationReason": null } + { + "name": "warning", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "unsupported", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } ], "possibleTypes": null }, diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx index 540191ac63b6c..741fc7be6614f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx @@ -11,7 +11,7 @@ import React, { useMemo } from 'react'; import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; -import { DocValueFields } from '../../../../common/search_strategy'; +import { Direction, DocValueFields } from '../../../../common/search_strategy'; export enum FirstLastSeenHostType { FIRST_SEEN = 'first-seen', @@ -31,7 +31,7 @@ export const FirstLastSeenHost = React.memo( docValueFields, hostName, indexNames, - order: type === FirstLastSeenHostType.FIRST_SEEN ? 'asc' : 'desc', + order: type === FirstLastSeenHostType.FIRST_SEEN ? Direction.asc : Direction.desc, }); const valueSeen = useMemo( () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen), diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx index db0dd5e230c87..6f43a18431a27 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/columns.tsx @@ -24,6 +24,7 @@ import { import { HostsTableColumns } from './'; import * as i18n from './translations'; +import { Maybe } from '../../../../common/search_strategy'; export const getHostsColumns = (): HostsTableColumns => [ { @@ -76,9 +77,13 @@ export const getHostsColumns = (): HostsTableColumns => [ truncateText: false, hideForMobile: false, sortable: true, - render: (lastSeen) => { + render: (lastSeen: Maybe | undefined) => { if (lastSeen != null) { - return ; + return ( + + ); } return getEmptyTagValue(); }, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index 922d52b6cfe5a..f88709e6e95ac 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -21,6 +21,8 @@ import { useGlobalFullScreen } from '../../../common/containers/use_full_screen' import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; @@ -96,6 +98,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ defaultModel={eventsDefaultModel} end={endDate} id={TimelineId.hostsPageEvents} + renderCellValue={DefaultCellRenderer} + rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} pageFilters={pageFilters} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index b5bf8594e7a8d..3c760545539c1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -595,6 +595,28 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'linux.advanced.diagnostic.enabled', + first_supported_version: '7.12', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.diagnostic.enabled', + { + defaultMessage: + "A value of 'false' disables running diagnostic features on Endpoint. Default: true.", + } + ), + }, + { + key: 'mac.advanced.diagnostic.enabled', + first_supported_version: '7.12', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.diagnostic.enabled', + { + defaultMessage: + "A value of 'false' disables running diagnostic features on Endpoint. Default: true.", + } + ), + }, { key: 'windows.advanced.alerts.cloud_lookup', first_supported_version: '7.12', diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index eceea1de4edc0..297746fd23632 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -12,15 +12,11 @@ import minimatch from 'minimatch'; import { IndexPatternMapping } from './types'; import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/public'; -import { +import type { + RenderTooltipContentParams, MapEmbeddable, MapEmbeddableInput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/maps/public/embeddable'; -import { - RenderTooltipContentParams, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/maps/public/classes/tooltips/tooltip_property'; +} from '../../../../../../plugins/maps/public'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts index 7d9c66261924b..6317cad7f8d98 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts @@ -5,8 +5,7 @@ * 2.0. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderTooltipContentParams } from '../../../../../maps/public/classes/tooltips/tooltip_property'; +import type { RenderTooltipContentParams } from '../../../../../maps/public'; export interface IndexPatternMapping { title: string; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index e63ffedf3da7c..459706de36569 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -14,6 +14,8 @@ import { StatefulTimeline } from '../../timeline'; import { TimelineId } from '../../../../../common/types/timeline'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; +import { defaultRowRenderers } from '../../timeline/body/renderers'; +import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer'; import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { @@ -46,7 +48,11 @@ const FlyoutPaneComponent: React.FC = ({ timelineId }) onClose={handleClose} size="l" > - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 705ddd62470a7..4d1c9e8037455 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -240,6 +240,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -350,6 +351,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -460,6 +462,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -568,6 +571,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -676,6 +680,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, savedObjectId: 'savedObject-1', columns: [ { @@ -852,6 +857,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, savedObjectId: 'savedObject-1', columns: [ { @@ -1000,6 +1006,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', @@ -1110,6 +1117,7 @@ describe('helpers', () => { const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); expect(newTimeline).toEqual({ activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: [ { columnHeaderType: 'not-filtered', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap index 72d2956bd4086..91d039a19495c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -22,26 +22,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 2

- @@ -63,15 +114,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 3

- @@ -93,15 +206,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 4

- @@ -123,15 +298,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 5

- @@ -153,15 +390,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 6

- @@ -183,15 +482,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 7

- @@ -213,15 +574,77 @@ exports[`Columns it renders the expected columns 1`] = ` You are in a table cell. row: 2, column: 8

- diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index f20978c6ba726..234e28e6231c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -9,10 +9,10 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import '../../../../../common/mock/match_media'; import { mockTimelineData } from '../../../../../common/mock'; import { defaultHeaders } from '../column_headers/default_headers'; -import { columnRenderers } from '../renderers'; import { DataDrivenColumns } from '.'; @@ -25,11 +25,11 @@ describe('Columns', () => { ariaRowindex={2} _id={mockTimelineData[0]._id} columnHeaders={headersSansTimestamp} - columnRenderers={columnRenderers} data={mockTimelineData[0].data} ecsData={mockTimelineData[0].ecs} hasRowRenderers={false} notesCount={0} + renderCellValue={DefaultCellRenderer} timelineId="test" /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index 5aba562749f01..aeb9af46ea2ec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -9,6 +9,7 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import React from 'react'; import { getOr } from 'lodash/fp'; +import { CellValueElementProps } from '../../cell_rendering'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -16,20 +17,19 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { EventsTd, EVENTS_TD_CLASS_NAME, EventsTdContent, EventsTdGroupData } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { getColumnRenderer } from '../renderers/get_column_renderer'; +import { StatefulCell } from './stateful_cell'; import * as i18n from './translations'; interface Props { _id: string; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; hasRowRenderers: boolean; notesCount: number; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; tabType?: TimelineTabs; timelineId: string; } @@ -82,11 +82,11 @@ export const DataDrivenColumns = React.memo( _id, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, hasRowRenderers, notesCount, + renderCellValue, tabType, timelineId, }) => ( @@ -105,18 +105,16 @@ export const DataDrivenColumns = React.memo(

{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: i + 2 })}

- {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} + {hasRowRenderers ? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx new file mode 100644 index 0000000000000..3c75bc7fb2649 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx @@ -0,0 +1,171 @@ +/* + * Copyright 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 { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React, { useEffect } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../store/timeline/model'; + +import { StatefulCell } from './stateful_cell'; +import { getMappedNonEcsValue } from '.'; + +/** + * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, + * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid + * + * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. + * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, + * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: + * https://codesandbox.io/s/zhxmo + */ +const RenderCellValue: React.FC = ({ columnId, data, setCellProps }) => { + useEffect(() => { + // branching logic that conditionally renders a specific cell green: + if (columnId === defaultHeaders[0].id) { + const value = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + + if (value?.length) { + setCellProps({ + style: { + backgroundColor: 'green', + }, + }); + } + } + }, [columnId, data, setCellProps]); + + return ( +
+ {getMappedNonEcsValue({ + data, + fieldName: columnId, + })} +
+ ); +}; + +describe('StatefulCell', () => { + const ariaRowindex = 123; + const eventId = '_id-123'; + const linkValues = ['foo', 'bar', '@baz']; + const tabType = TimelineTabs.query; + const timelineId = 'test'; + + let header: ColumnHeaderOptions; + let data: TimelineNonEcsData[]; + beforeEach(() => { + data = cloneDeep(mockTimelineData[0].data); + header = cloneDeep(defaultHeaders[0]); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId: `${timelineId}-${tabType}`, + }) + ); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { + const renderCellValue = jest.fn(); + + mount( + + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId, + }) + ); + }); + + test('it renders the React.Node returned by renderCellValue', () => { + const renderCellValue = () =>
; + + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); + }); + + test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') + ).toEqual('background-color: green;'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx new file mode 100644 index 0000000000000..83f603364ba8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 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, { HTMLAttributes, useState } from 'react'; + +import { CellValueElementProps } from '../../cell_rendering'; +import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; +import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; + +export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; +} + +const StatefulCellComponent = ({ + ariaRowindex, + data, + header, + eventId, + linkValues, + renderCellValue, + tabType, + timelineId, +}: { + ariaRowindex: number; + data: TimelineNonEcsData[]; + header: ColumnHeaderOptions; + eventId: string; + linkValues: string[] | undefined; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; +}) => { + const [cellProps, setCellProps] = useState>({}); + + return ( +
+ {renderCellValue({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + setCellProps, + timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, + })} +
+ ); +}; + +StatefulCellComponent.displayName = 'StatefulCellComponent'; + +export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index abdfda3272d6a..74724dedf4d11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -14,6 +14,7 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import * as i18n from '../translations'; import { EventColumnView } from './event_column_view'; +import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'; import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; @@ -56,6 +57,7 @@ describe('EventColumnView', () => { onRowSelected: jest.fn(), onUnPinEvent: jest.fn(), refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, selectedEventIds: {}, showCheckboxes: false, showNotes: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index c6caf0a7b5b15..a0a0aeb23e8f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useMemo } from 'react'; +import { CellValueElementProps } from '../../cell_rendering'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; @@ -21,7 +22,6 @@ import { getPinOnClick, InvestigateInResolverAction, } from '../helpers'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { AlertContextMenu } from '../../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { InvestigateInTimelineAction } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action'; import { AddEventNoteAction } from '../actions/add_note_icon_item'; @@ -38,7 +38,6 @@ interface Props { actionsColumnWidth: number; ariaRowindex: number; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; ecsData: Ecs; eventIdToNoteIds: Readonly>; @@ -51,6 +50,7 @@ interface Props { onRowSelected: OnRowSelected; onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; hasRowRenderers: boolean; selectedEventIds: Readonly>; @@ -69,7 +69,6 @@ export const EventColumnView = React.memo( actionsColumnWidth, ariaRowindex, columnHeaders, - columnRenderers, data, ecsData, eventIdToNoteIds, @@ -84,6 +83,7 @@ export const EventColumnView = React.memo( refetch, hasRowRenderers, onRuleChange, + renderCellValue, selectedEventIds, showCheckboxes, showNotes, @@ -227,11 +227,11 @@ export const EventColumnView = React.memo( _id={id} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} ecsData={ecsData} hasRowRenderers={hasRowRenderers} notesCount={notesCount} + renderCellValue={renderCellValue} tabType={tabType} timelineId={timelineId} /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index d76b5834c233e..7f8a3a92fb5ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { isEmpty } from 'lodash'; +import { CellValueElementProps } from '../../cell_rendering'; import { inputsModel } from '../../../../../common/store'; import { BrowserFields } from '../../../../../common/containers/source'; import { @@ -18,7 +19,6 @@ import { TimelineTabs } from '../../../../../../common/types/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { StatefulEvent } from './stateful_event'; import { eventIsPinned } from '../helpers'; @@ -30,7 +30,6 @@ interface Props { actionsColumnWidth: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; containerRef: React.MutableRefObject; data: TimelineItem[]; eventIdToNoteIds: Readonly>; @@ -41,6 +40,7 @@ interface Props { onRowSelected: OnRowSelected; pinnedEventIds: Readonly>; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; @@ -52,7 +52,6 @@ const EventsComponent: React.FC = ({ actionsColumnWidth, browserFields, columnHeaders, - columnRenderers, containerRef, data, eventIdToNoteIds, @@ -64,6 +63,7 @@ const EventsComponent: React.FC = ({ pinnedEventIds, refetch, onRuleChange, + renderCellValue, rowRenderers, selectedEventIds, showCheckboxes, @@ -76,7 +76,6 @@ const EventsComponent: React.FC = ({ ariaRowindex={i + ARIA_ROW_INDEX_OFFSET} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} containerRef={containerRef} event={event} eventIdToNoteIds={eventIdToNoteIds} @@ -88,6 +87,7 @@ const EventsComponent: React.FC = ({ lastFocusedAriaColindex={lastFocusedAriaColindex} loadingEventIds={loadingEventIds} onRowSelected={onRowSelected} + renderCellValue={renderCellValue} refetch={refetch} rowRenderers={rowRenderers} onRuleChange={onRuleChange} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 4191badd6b03f..97ab088b61583 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { CellValueElementProps } from '../../cell_rendering'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { TimelineExpandedDetailType, @@ -23,7 +24,6 @@ import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/mod import { OnPinEvent, OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; @@ -45,7 +45,6 @@ interface Props { containerRef: React.MutableRefObject; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; isEventViewer?: boolean; @@ -56,6 +55,7 @@ interface Props { refetch: inputsModel.Refetch; ariaRowindex: number; onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; selectedEventIds: Readonly>; showCheckboxes: boolean; @@ -77,7 +77,6 @@ const StatefulEventComponent: React.FC = ({ browserFields, containerRef, columnHeaders, - columnRenderers, event, eventIdToNoteIds, isEventViewer = false, @@ -86,8 +85,9 @@ const StatefulEventComponent: React.FC = ({ loadingEventIds, onRowSelected, refetch, - onRuleChange, + renderCellValue, rowRenderers, + onRuleChange, ariaRowindex, selectedEventIds, showCheckboxes, @@ -259,7 +259,6 @@ const StatefulEventComponent: React.FC = ({ actionsColumnWidth={actionsColumnWidth} ariaRowindex={ariaRowindex} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={event.data} ecsData={event.ecs} eventIdToNoteIds={eventIdToNoteIds} @@ -273,6 +272,7 @@ const StatefulEventComponent: React.FC = ({ onRowSelected={onRowSelected} onUnPinEvent={onUnPinEvent} refetch={refetch} + renderCellValue={renderCellValue} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} showCheckboxes={showCheckboxes} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 723e4c3de5c27..76dbfc553d228 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import '../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { Direction } from '../../../../../common/search_strategy'; @@ -19,6 +20,7 @@ import { Sort } from './sort'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; +import { defaultRowRenderers } from './renderers'; const mockSort: Sort[] = [ { @@ -39,8 +41,8 @@ jest.mock('react-redux', () => { }); jest.mock('../../../../common/hooks/use_selector', () => ({ - useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), - useDeepEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), + useShallowEqualSelector: () => mockTimelineModel, + useDeepEqualSelector: () => mockTimelineModel, })); jest.mock('../../../../common/components/link_to'); @@ -76,6 +78,8 @@ describe('Body', () => { loadingEventIds: [], pinnedEventIds: {}, refetch: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, selectedEventIds: {}, setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], sort: mockSort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 4df6eb16ccb62..59c0610c544e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { CellValueElementProps } from '../cell_rendering'; import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { FIRST_ARIA_INDEX, @@ -28,9 +29,9 @@ import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; import { getEventIdToDataMapping } from './helpers'; -import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { plainRowRenderer } from './renderers/plain_row_renderer'; +import { RowRenderer } from './renderers/row_renderer'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { Events } from './events'; @@ -44,6 +45,8 @@ interface OwnProps { isEventViewer?: boolean; sort: Sort[]; refetch: inputsModel.Refetch; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; tabType: TimelineTabs; totalPages: number; onRuleChange?: () => void; @@ -83,6 +86,8 @@ export const BodyComponent = React.memo( onRuleChange, showCheckboxes, refetch, + renderCellValue, + rowRenderers, sort, tabType, totalPages, @@ -141,7 +146,7 @@ export const BodyComponent = React.memo( if (!excludedRowRendererIds) return rowRenderers; return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); - }, [excludedRowRendererIds]); + }, [excludedRowRendererIds, rowRenderers]); const actionsColumnWidth = useMemo( () => @@ -209,7 +214,6 @@ export const BodyComponent = React.memo( actionsColumnWidth={actionsColumnWidth} browserFields={browserFields} columnHeaders={columnHeaders} - columnRenderers={columnRenderers} data={data} eventIdToNoteIds={eventIdToNoteIds} id={id} @@ -219,6 +223,7 @@ export const BodyComponent = React.memo( onRowSelected={onRowSelected} pinnedEventIds={pinnedEventIds} refetch={refetch} + renderCellValue={renderCellValue} rowRenderers={enabledRowRenderers} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} @@ -244,6 +249,8 @@ export const BodyComponent = React.memo( prevProps.id === nextProps.id && prevProps.isEventViewer === nextProps.isEventViewer && prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.tabType === nextProps.tabType ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 6e36102da2de9..b92a4381d837b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -17,7 +17,7 @@ import { mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; -import { rowRenderers } from '.'; +import { defaultRowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; jest.mock('@elastic/eui', () => { @@ -48,7 +48,7 @@ describe('get_column_renderer', () => { }); test('renders correctly against snapshot', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -60,7 +60,7 @@ describe('get_column_renderer', () => { }); test('should render plain row data when it is a non suricata row', () => { - const rowRenderer = getRowRenderer(nonSuricata, rowRenderers); + const rowRenderer = getRowRenderer(nonSuricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: nonSuricata, @@ -75,7 +75,7 @@ describe('get_column_renderer', () => { }); test('should render a suricata row data when it is a suricata row', () => { - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -93,7 +93,7 @@ describe('get_column_renderer', () => { test('should render a suricata row data if event.category is network_traffic', () => { suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(suricata, rowRenderers); + const rowRenderer = getRowRenderer(suricata, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: suricata, @@ -111,7 +111,7 @@ describe('get_column_renderer', () => { test('should render a zeek row data if event.category is network_traffic', () => { zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(zeek, rowRenderers); + const rowRenderer = getRowRenderer(zeek, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: zeek, @@ -129,7 +129,7 @@ describe('get_column_renderer', () => { test('should render a system row data if event.category is network_traffic', () => { system.event = { ...system.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(system, rowRenderers); + const rowRenderer = getRowRenderer(system, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: system, @@ -147,7 +147,7 @@ describe('get_column_renderer', () => { test('should render a auditd row data if event.category is network_traffic', () => { auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; - const rowRenderer = getRowRenderer(auditd, rowRenderers); + const rowRenderer = getRowRenderer(auditd, defaultRowRenderers); const row = rowRenderer?.renderRow({ browserFields: mockBrowserFields, data: auditd, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 671d183c62e6d..209a9414f62f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -23,7 +23,7 @@ import { systemRowRenderers } from './system/generic_row_renderer'; // Suricata and Zeek which is why Suricata and Zeek are above it. The // plainRowRenderer always returns true to everything which is why it always // should be last. -export const rowRenderers: RowRenderer[] = [ +export const defaultRowRenderers: RowRenderer[] = [ ...auditdRowRenderers, ...systemRowRenderers, suricataRowRenderer, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx new file mode 100644 index 0000000000000..5ac1dcf8805cf --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; +import { DragDropContextWrapper } from '../../../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; +import { DefaultCellRenderer } from './default_cell_renderer'; + +jest.mock('../body/renderers/get_column_renderer'); +const getColumnRendererMock = getColumnRenderer as jest.Mock; +const mockImplementation = { + renderColumn: jest.fn(), +}; + +describe('DefaultCellRenderer', () => { + const columnId = 'signal.rule.risk_score'; + const eventId = '_id-123'; + const isDetails = true; + const isExpandable = true; + const isExpanded = true; + const linkValues = ['foo', 'bar', '@baz']; + const rowIndex = 3; + const setCellProps = jest.fn(); + const timelineId = 'test'; + + beforeEach(() => { + jest.clearAllMocks(); + getColumnRendererMock.mockImplementation(() => mockImplementation); + }); + + test('it invokes `getColumnRenderer` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(getColumnRenderer).toBeCalledWith(header.id, columnRenderers, data); + }); + + test('it invokes `renderColumn` with the expected arguments', () => { + const data = cloneDeep(mockTimelineData[0].data); + const header = cloneDeep(defaultHeaders[0]); + + mount( + + + + + + + + ); + + expect(mockImplementation.renderColumn).toBeCalledWith({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: ['2018-11-05T19:03:25.937Z'], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx new file mode 100644 index 0000000000000..8d8f821107e7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { getMappedNonEcsValue } from '../body/data_driven_columns'; +import { columnRenderers } from '../body/renderers'; +import { getColumnRenderer } from '../body/renderers/get_column_renderer'; + +import { CellValueElementProps } from '.'; + +export const DefaultCellRenderer: React.FC = ({ + columnId, + data, + eventId, + header, + linkValues, + setCellProps, + timelineId, +}) => ( + <> + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId, + field: header, + linkValues, + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx new file mode 100644 index 0000000000000..03e444e3a9afd --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; + +import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; +import { ColumnHeaderOptions } from '../../../store/timeline/model'; + +/** The following props are provided to the function called by `renderCellValue` */ +export type CellValueElementProps = EuiDataGridCellValueElementProps & { + data: TimelineNonEcsData[]; + eventId: string; // _id + header: ColumnHeaderOptions; + linkValues: string[] | undefined; + timelineId: string; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap index 2595f29144b80..7d237ecaf92df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/__snapshots__/index.test.tsx.snap @@ -140,6 +140,986 @@ In other use cases the message field can be used to concatenate different values ] } onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } showExpandedDetails={false} start="2018-03-23T18:49:23.132Z" timelineId="test" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index 7b77a915f2f05..e13bed1e2eff6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -9,6 +9,8 @@ import { shallow } from 'enzyme'; import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; +import { defaultRowRenderers } from '../body/renderers'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; @@ -94,6 +96,8 @@ describe('Timeline', () => { itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showExpandedDetails: false, start: startDate, timerangeKind: 'absolute', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 51f8db4e796e5..6bb19ce5a6852 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -22,10 +22,12 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { calculateTotalPages } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; @@ -133,6 +135,8 @@ const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.timerangeKind === nextProps.timerangeKind; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -154,6 +158,8 @@ export const EqlTabContentComponent: React.FC = ({ itemsPerPage, itemsPerPageOptions, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, start, timerangeKind, @@ -284,6 +290,8 @@ export const EqlTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={NO_SORTING} tabType={TimelineTabs.eql} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index ee2ce8cf8103b..db7a3cc3c9900 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -17,7 +17,9 @@ import { mockIndexNames, mockIndexPattern, TestProviders } from '../../../common import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; +import { DefaultCellRenderer } from './cell_rendering/default_cell_renderer'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles'; +import { defaultRowRenderers } from './body/renderers'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -63,6 +65,8 @@ jest.mock('../../../common/containers/sourcerer', () => { }); describe('StatefulTimeline', () => { const props: StatefulTimelineOwnProps = { + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, timelineId: TimelineId.test, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 6d2374dd8eef7..367357511c9c8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -14,6 +14,8 @@ import styled from 'styled-components'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; +import { RowRenderer } from './body/renderers/row_renderer'; +import { CellValueElementProps } from './cell_rendering'; import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -36,10 +38,12 @@ const TimelineTemplateBadge = styled.div` `; export interface Props { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: TimelineId; } -const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { +const TimelineSavingProgressComponent: React.FC<{ timelineId: TimelineId }> = ({ timelineId }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const isSaving = useShallowEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isSaving @@ -50,7 +54,11 @@ const TimelineSavingProgressComponent: React.FC = ({ timelineId }) => { const TimelineSavingProgress = React.memo(TimelineSavingProgressComponent); -const StatefulTimelineComponent: React.FC = ({ timelineId }) => { +const StatefulTimelineComponent: React.FC = ({ + renderCellValue, + rowRenderers, + timelineId, +}) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -131,6 +139,8 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { { timelineId: TimelineId.test, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, sort, pinnedEventIds: {}, showExpandedDetails: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index a19a61d8268ff..dfc14747dacf3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -14,10 +14,12 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; +import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -87,6 +89,8 @@ const VerticalRule = styled.div` VerticalRule.displayName = 'VerticalRule'; interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -106,6 +110,8 @@ export const PinnedTabContentComponent: React.FC = ({ itemsPerPageOptions, pinnedEventIds, onEventClosed, + renderCellValue, + rowRenderers, showExpandedDetails, sort, }) => { @@ -217,6 +223,8 @@ export const PinnedTabContentComponent: React.FC = ({ data={events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.pinned} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap index 0688a10b31eef..46c85f634ff6b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/__snapshots__/index.test.tsx.snap @@ -276,6 +276,986 @@ In other use cases the message field can be used to concatenate different values kqlMode="search" kqlQueryExpression="" onEventClosed={[MockFunction]} + renderCellValue={[Function]} + rowRenderers={ + Array [ + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "auditd", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_dns", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "alerts", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "library", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "registry", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_fim", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_security_event", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_file", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system_socket", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "system", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "suricata", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "zeek", + "isInstance": [Function], + "renderRow": [Function], + }, + Object { + "id": "netflow", + "isInstance": [Function], + "renderRow": [Function], + }, + ] + } show={true} showCallOutUnauthorizedMsg={false} showExpandedDetails={false} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index c7d27da64c650..ede473acbfb2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -10,11 +10,13 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import { Direction } from '../../../../graphql/types'; +import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer'; import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock/test_providers'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps } from './index'; +import { defaultRowRenderers } from '../body/renderers'; import { Sort } from '../body/sort'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; @@ -106,6 +108,8 @@ describe('Timeline', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, sort, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 28fec7ded9ca2..74a0f02354219 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -22,6 +22,8 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { Direction, TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { useKibana } from '../../../../common/lib/kibana'; @@ -142,6 +144,8 @@ const compareQueryProps = (prevProps: Props, nextProps: Props) => deepEqual(prevProps.filters, nextProps.filters); interface OwnProps { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; timelineId: string; } @@ -164,6 +168,8 @@ export const QueryTabContentComponent: React.FC = ({ kqlMode, kqlQueryExpression, onEventClosed, + renderCellValue, + rowRenderers, show, showCallOutUnauthorizedMsg, showExpandedDetails, @@ -330,6 +336,8 @@ export const QueryTabContentComponent: React.FC = ({ data={isBlankTimeline ? EMPTY_EVENTS : events} id={timelineId} refetch={refetch} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} sort={sort} tabType={TimelineTabs.query} totalPages={calculateTotalPages({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index f29211d519841..76a2ad0960322 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -20,6 +20,8 @@ import { TimelineEventsCountBadge, } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; +import { RowRenderer } from '../body/renderers/row_renderer'; +import { CellValueElementProps } from '../cell_rendering'; import { getActiveTabSelector, getNoteIdsSelector, @@ -46,6 +48,8 @@ const NotesTabContent = lazy(() => import('../notes_tab_content')); const PinnedTabContent = lazy(() => import('../pinned_tab_content')); interface BasicTimelineTab { + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; setTimelineFullScreen?: (fullScreen: boolean) => void; timelineFullScreen?: boolean; timelineId: TimelineId; @@ -53,16 +57,32 @@ interface BasicTimelineTab { graphEventId?: string; } -const QueryTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const QueryTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); QueryTab.displayName = 'QueryTab'; -const EqlTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const EqlTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); EqlTab.displayName = 'EqlTab'; @@ -81,9 +101,17 @@ const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => )); NotesTab.displayName = 'NotesTab'; -const PinnedTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => ( +const PinnedTab: React.FC<{ + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + timelineId: TimelineId; +}> = memo(({ renderCellValue, rowRenderers, timelineId }) => ( }> - + )); PinnedTab.displayName = 'PinnedTab'; @@ -91,7 +119,7 @@ PinnedTab.displayName = 'PinnedTab'; type ActiveTimelineTabProps = BasicTimelineTab & { activeTimelineTab: TimelineTabs }; const ActiveTimelineTab = memo( - ({ activeTimelineTab, timelineId, timelineType }) => { + ({ activeTimelineTab, renderCellValue, rowRenderers, timelineId, timelineType }) => { const getTab = useCallback( (tab: TimelineTabs) => { switch (tab) { @@ -119,14 +147,26 @@ const ActiveTimelineTab = memo( return ( <> - + - + {timelineType === TimelineType.default && ( - + )} @@ -160,6 +200,8 @@ const StyledEuiTab = styled(EuiTab)` `; const TabsContentComponent: React.FC = ({ + renderCellValue, + rowRenderers, timelineId, timelineFullScreen, timelineType, @@ -300,6 +342,8 @@ const TabsContentComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index 7f38de0cebbd5..b24a50a516325 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -208,4 +208,35 @@ describe('useTimelineEvents', () => { ]); }); }); + + test('Correlation pagination is calling search strategy when switching page', async () => { + await act(async () => { + const { result, waitForNextUpdate, rerender } = renderHook< + UseTimelineEventsProps, + [boolean, TimelineArgs] + >((args) => useTimelineEvents(args), { + initialProps: { + ...props, + language: 'eql', + eqlOptions: { + eventCategoryField: 'category', + tiebreakerField: '', + timestampField: '@timestamp', + query: 'find it EQL', + size: 100, + }, + }, + }); + + // useEffect on params request + await waitForNextUpdate(); + rerender({ ...props, startDate, endDate }); + // useEffect on params request + await waitForNextUpdate(); + expect(mockSearch).toHaveBeenCalledTimes(2); + result.current[1].loadPage(4); + await waitForNextUpdate(); + expect(mockSearch).toHaveBeenCalledTimes(3); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 38fa81a4fb7c2..ab4b4358fd326 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -143,7 +143,6 @@ export const useTimelineEvents = ({ activeTimeline.setExpandedDetail({}); activeTimeline.setActivePage(newActivePage); } - setActivePage(newActivePage); }, [clearSignalsState, id] @@ -294,22 +293,22 @@ export const useTimelineEvents = ({ querySize: prevRequest?.pagination.querySize ?? 0, sort: prevRequest?.sort ?? initSortDefault, timerange: prevRequest?.timerange ?? {}, - ...(prevEqlRequest?.eventCategoryField + ...(!isEmpty(prevEqlRequest?.eventCategoryField) ? { eventCategoryField: prevEqlRequest?.eventCategoryField, } : {}), - ...(prevEqlRequest?.size + ...(!isEmpty(prevEqlRequest?.size) ? { size: prevEqlRequest?.size, } : {}), - ...(prevEqlRequest?.tiebreakerField + ...(!isEmpty(prevEqlRequest?.tiebreakerField) ? { tiebreakerField: prevEqlRequest?.tiebreakerField, } : {}), - ...(prevEqlRequest?.timestampField + ...(!isEmpty(prevEqlRequest?.timestampField) ? { timestampField: prevEqlRequest?.timestampField, } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 5f9e64843573f..df79ff1d2b309 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -18,6 +18,7 @@ const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false) export const timelineDefaults: SubsetTimelineModel & Pick = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.query, columns: defaultHeaders, dataProviders: [], dateRange: { start, end }, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts index 57fa86f853c8d..0bc1c5d57fa33 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.test.ts @@ -16,6 +16,7 @@ describe('Epic Timeline', () => { test('should return a TimelineInput instead of TimelineModel ', () => { const timelineModel: TimelineModel = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.notes, columns: [ { columnHeaderType: 'not-filtered', diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 3d92397f4ab50..0b70ba8991686 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -30,11 +30,12 @@ import { updateItemsPerPage, updateSort, } from './actions'; - +import { DefaultCellRenderer } from '../../components/timeline/cell_rendering/default_cell_renderer'; import { QueryTabContentComponent, Props as QueryTabContentComponentProps, } from '../../components/timeline/query_tab_content'; +import { defaultRowRenderers } from '../../components/timeline/body/renderers'; import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; import { Sort } from '../../components/timeline/body/sort'; import { Direction } from '../../../graphql/types'; @@ -90,6 +91,8 @@ describe('epicLocalStorage', () => { kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', onEventClosed: jest.fn(), + renderCellValue: DefaultCellRenderer, + rowRenderers: defaultRowRenderers, showCallOutUnauthorizedMsg: false, showExpandedDetails: false, start: startDate, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 864e52fc377a0..135cbb3f73281 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -305,6 +305,9 @@ export const updateGraphEventId = ({ [id]: { ...timeline, graphEventId, + ...(graphEventId === '' && id === TimelineId.active + ? { activeTab: timeline.prevActiveTab, prevActiveTab: timeline.activeTab } + : {}), }, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index b1ff4a1e89729..a899994ad4aab 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -51,6 +51,7 @@ export interface ColumnHeaderOptions { export interface TimelineModel { /** The selected tab to displayed in the timeline */ activeTab: TimelineTabs; + prevActiveTab: TimelineTabs; /** The columns displayed in the timeline */ columns: ColumnHeaderOptions[]; /** Timeline saved object owner */ @@ -142,6 +143,7 @@ export type SubsetTimelineModel = Readonly< Pick< TimelineModel, | 'activeTab' + | 'prevActiveTab' | 'columns' | 'dataProviders' | 'deletedEventIds' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index acdf064c2355f..e464637c469f8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -6,7 +6,12 @@ */ import { cloneDeep } from 'lodash/fp'; -import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; +import { + TimelineType, + TimelineStatus, + TimelineTabs, + TimelineId, +} from '../../../../common/types/timeline'; import { IS_OPERATOR, @@ -39,6 +44,7 @@ import { updateTimelineSort, updateTimelineTitleAndDescription, upsertTimelineColumn, + updateGraphEventId, } from './helpers'; import { ColumnHeaderOptions, TimelineModel } from './model'; import { timelineDefaults } from './defaults'; @@ -69,6 +75,7 @@ const basicDataProvider: DataProvider = { }; const basicTimeline: TimelineModel = { activeTab: TimelineTabs.query, + prevActiveTab: TimelineTabs.graph, columns: [], dataProviders: [{ ...basicDataProvider }], dateRange: { @@ -1757,4 +1764,55 @@ describe('Timeline', () => { ]); }); }); + + describe('#updateGraphEventId', () => { + test('should return a new reference and not the same reference', () => { + const update = updateGraphEventId({ + id: 'foo', + graphEventId: '123', + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should empty graphEventId', () => { + const update = updateGraphEventId({ + id: 'foo', + graphEventId: '', + timelineById: timelineByIdMock, + }); + expect(update.foo.graphEventId).toEqual(''); + }); + + test('should empty graphEventId and not change activeTab and prevActiveTab because TimelineId !== TimelineId.active', () => { + const update = updateGraphEventId({ + id: 'foo', + graphEventId: '', + timelineById: timelineByIdMock, + }); + expect(update.foo.graphEventId).toEqual(''); + expect(update.foo.activeTab).toEqual(timelineByIdMock.foo.activeTab); + expect(update.foo.prevActiveTab).toEqual(timelineByIdMock.foo.prevActiveTab); + }); + + test('should empty graphEventId and return to the previous tab if TimelineId === TimelineId.active', () => { + const mock = cloneDeep(timelineByIdMock); + mock[TimelineId.active] = { + ...timelineByIdMock.foo, + activeTab: TimelineTabs.graph, + prevActiveTab: TimelineTabs.eql, + }; + delete mock.foo; + + const update = updateGraphEventId({ + id: TimelineId.active, + graphEventId: '', + timelineById: mock, + }); + + expect(update[TimelineId.active].graphEventId).toEqual(''); + expect(update[TimelineId.active].activeTab).toEqual(TimelineTabs.eql); + expect(update[TimelineId.active].prevActiveTab).toEqual(TimelineTabs.graph); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 332d9ad4ba91b..80c6d83075719 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -526,6 +526,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) [id]: { ...state.timelineById[id], activeTab, + prevActiveTab: state.timelineById[id].activeTab, }, }, })) diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 29b18ba3f5bf5..65bd6ffd15f5f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -15,7 +15,7 @@ import { export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', /** - * Saved objects no longer used for storing artifacts. Value + * Saved objects no longer used for storing artifacts * @deprecated */ SAVED_OBJECT_TYPE: 'endpoint:user-artifact', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts index ed945347373e5..c70dd39e17e9e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.test.ts @@ -171,7 +171,7 @@ describe('test alerts route', () => { // and this entire test file refactored to start using fleet's exposed FleetArtifactClient class. endpointAppContextService! .getManifestManager()! - .getArtifactsClient().getArtifact = jest.fn().mockResolvedValue(soFindResp); + .getArtifactsClient().getArtifact = jest.fn().mockResolvedValue(soFindResp.attributes); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/artifacts/download') diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts index 99a39616195dd..948cd035243bd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_artifact.ts @@ -91,9 +91,9 @@ export function registerDownloadArtifactRoute( return res.notFound({ body: `No artifact found for ${id}` }); } - const bodyBuffer = Buffer.from(artifact.attributes.body, 'base64'); + const bodyBuffer = Buffer.from(artifact.body, 'base64'); cache.set(id, bodyBuffer); - return buildAndValidateResponse(artifact.attributes.identifier, bodyBuffer); + return buildAndValidateResponse(artifact.identifier, bodyBuffer); } } ); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts index b3f098a969336..1dcac108338bb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts @@ -5,48 +5,49 @@ * 2.0. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; -import { ArtifactClient } from './artifact_client'; +import { EndpointArtifactClient } from './artifact_client'; +import { createArtifactsClientMock } from '../../../../../fleet/server/mocks'; describe('artifact_client', () => { describe('ArtifactClient sanity checks', () => { + let fleetArtifactClient: ReturnType; + let artifactClient: EndpointArtifactClient; + + beforeEach(() => { + fleetArtifactClient = createArtifactsClientMock(); + artifactClient = new EndpointArtifactClient(fleetArtifactClient); + }); + test('can create ArtifactClient', () => { - const artifactClient = new ArtifactClient(savedObjectsClientMock.create()); - expect(artifactClient).toBeInstanceOf(ArtifactClient); + expect(artifactClient).toBeInstanceOf(EndpointArtifactClient); }); test('can get artifact', async () => { - const savedObjectsClient = savedObjectsClientMock.create(); - const artifactClient = new ArtifactClient(savedObjectsClient); await artifactClient.getArtifact('abcd'); - expect(savedObjectsClient.get).toHaveBeenCalled(); + expect(fleetArtifactClient.listArtifacts).toHaveBeenCalled(); }); test('can create artifact', async () => { - const savedObjectsClient = savedObjectsClientMock.create(); - const artifactClient = new ArtifactClient(savedObjectsClient); - const artifact = await getInternalArtifactMock('linux', 'v1'); + const artifact = await getInternalArtifactMock('linux', 'v1', { compress: true }); await artifactClient.createArtifact(artifact); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - ArtifactConstants.SAVED_OBJECT_TYPE, - { - ...artifact, - created: expect.any(Number), - }, - { id: getArtifactId(artifact) } - ); + expect(fleetArtifactClient.createArtifact).toHaveBeenCalledWith({ + identifier: artifact.identifier, + type: 'exceptionlist', + content: + '{"entries":[{"type":"simple","entries":[{"entries":[{"field":"some.nested.field","operator":"included","type":"exact_cased","value":"some value"}],' + + '"field":"some.parentField","type":"nested"},{"field":"some.not.nested.field","operator":"included","type":"exact_cased","value":"some value"}]},' + + '{"type":"simple","entries":[{"field":"some.other.not.nested.field","operator":"included","type":"exact_cased","value":"some other value"}]}]}', + }); }); test('can delete artifact', async () => { - const savedObjectsClient = savedObjectsClientMock.create(); - const artifactClient = new ArtifactClient(savedObjectsClient); - await artifactClient.deleteArtifact('abcd'); - expect(savedObjectsClient.delete).toHaveBeenCalledWith( - ArtifactConstants.SAVED_OBJECT_TYPE, - 'abcd' - ); + await artifactClient.deleteArtifact('endpoint-trustlist-linux-v1-sha26hash'); + expect(fleetArtifactClient.listArtifacts).toHaveBeenCalledWith({ + kuery: `decoded_sha256: "sha26hash" AND identifier: "endpoint-trustlist-linux-v1"`, + perPage: 1, + }); + expect(fleetArtifactClient.deleteArtifact).toHaveBeenCalledWith('123'); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts index d9a2e86159d6c..ef48ed1dd43f6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts @@ -5,64 +5,23 @@ * 2.0. */ -/* eslint-disable max-classes-per-file */ - import { inflate as _inflate } from 'zlib'; import { promisify } from 'util'; -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { ArtifactConstants, getArtifactId } from '../../lib/artifacts'; -import { - InternalArtifactCompleteSchema, - InternalArtifactCreateSchema, -} from '../../schemas/artifacts'; +import { InternalArtifactCompleteSchema } from '../../schemas/artifacts'; import { Artifact, ArtifactsClientInterface } from '../../../../../fleet/server'; const inflateAsync = promisify(_inflate); export interface EndpointArtifactClientInterface { - getArtifact(id: string): Promise | undefined>; + getArtifact(id: string): Promise; - createArtifact( - artifact: InternalArtifactCompleteSchema - ): Promise>; + createArtifact(artifact: InternalArtifactCompleteSchema): Promise; deleteArtifact(id: string): Promise; } -export class ArtifactClient implements EndpointArtifactClientInterface { - private savedObjectsClient: SavedObjectsClientContract; - - constructor(savedObjectsClient: SavedObjectsClientContract) { - this.savedObjectsClient = savedObjectsClient; - } - - public async getArtifact(id: string): Promise> { - return this.savedObjectsClient.get( - ArtifactConstants.SAVED_OBJECT_TYPE, - id - ); - } - - public async createArtifact( - artifact: InternalArtifactCompleteSchema - ): Promise> { - return this.savedObjectsClient.create( - ArtifactConstants.SAVED_OBJECT_TYPE, - { - ...artifact, - created: Date.now(), - }, - { id: getArtifactId(artifact) } - ); - } - - public async deleteArtifact(id: string) { - await this.savedObjectsClient.delete(ArtifactConstants.SAVED_OBJECT_TYPE, id); - } -} - /** - * Endpoint specific artifact managment client which uses FleetArtifactsClient to persist artifacts + * Endpoint specific artifact management client which uses FleetArtifactsClient to persist artifacts * to the Fleet artifacts index (then used by Fleet Server) */ export class EndpointArtifactClient implements EndpointArtifactClientInterface { @@ -91,15 +50,12 @@ export class EndpointArtifactClient implements EndpointArtifactClientInterface { return; } - // FIXME:PT change method signature so that it returns back only the `InternalArtifactCompleteSchema` - return ({ - attributes: artifacts.items[0], - } as unknown) as SavedObject; + return artifacts.items[0]; } async createArtifact( artifact: InternalArtifactCompleteSchema - ): Promise> { + ): Promise { // FIXME:PT refactor to make this more efficient by passing through the uncompressed artifact content // Artifact `.body` is compressed/encoded. We need it decoded and as a string const artifactContent = await inflateAsync(Buffer.from(artifact.body, 'base64')); @@ -110,15 +66,13 @@ export class EndpointArtifactClient implements EndpointArtifactClientInterface { type: this.parseArtifactId(artifact.identifier).type, }); - return ({ - attributes: createdArtifact, - } as unknown) as SavedObject; + return createdArtifact; } async deleteArtifact(id: string) { // Ignoring the `id` not being in the type until we can refactor the types in endpoint. // @ts-ignore - const artifactId = (await this.getArtifact(id)).attributes?.id; + const artifactId = (await this.getArtifact(id))?.id!; return this.fleetArtifacts.deleteArtifact(artifactId); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index ca0088e834c3a..ececb425af657 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -20,8 +20,7 @@ import { getMockArtifactsWithDiff, getEmptyMockArtifacts, } from '../../../lib/artifacts/mocks'; -import { ArtifactClient } from '../artifact_client'; -import { getManifestClientMock } from '../mocks'; +import { createEndpointArtifactClientMock, getManifestClientMock } from '../mocks'; import { ManifestManager, ManifestManagerContext } from './manifest_manager'; export const createExceptionListResponse = (data: ExceptionListItemSchema[], total?: number) => ({ @@ -84,7 +83,7 @@ export const buildManifestManagerContextMock = ( return { ...fullOpts, - artifactClient: new ArtifactClient(fullOpts.savedObjectsClient), + artifactClient: createEndpointArtifactClientMock(), logger: loggingSystemMock.create().get() as jest.Mocked, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index a4efdbc75fb16..423cd4fddd0aa 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -6,7 +6,6 @@ */ import { inflateSync } from 'zlib'; -import { SavedObjectsErrorHelpers } from 'src/core/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -23,7 +22,6 @@ import { toArtifactRecords, } from '../../../lib/artifacts/mocks'; import { - ArtifactConstants, ManifestConstants, getArtifactId, isCompressed, @@ -37,6 +35,7 @@ import { } from './manifest_manager.mock'; import { ManifestManager } from './manifest_manager'; +import { EndpointArtifactClientInterface } from '../artifact_client'; const uncompressData = async (data: Buffer) => JSON.parse(await inflateSync(data).toString()); @@ -145,9 +144,8 @@ describe('ManifestManager', () => { test('Retrieves non empty manifest successfully', async () => { const savedObjectsClient = savedObjectsClientMock.create(); - const manifestManager = new ManifestManager( - buildManifestManagerContextMock({ savedObjectsClient }) - ); + const manifestManagerContext = buildManifestManagerContextMock({ savedObjectsClient }); + const manifestManager = new ManifestManager(manifestManagerContext); savedObjectsClient.get = jest .fn() @@ -169,13 +167,17 @@ describe('ManifestManager', () => { }, version: '2.0.0', }; - } else if (objectType === ArtifactConstants.SAVED_OBJECT_TYPE) { - return { attributes: ARTIFACTS_BY_ID[id], version: '2.1.1' }; } else { return null; } }); + (manifestManagerContext.artifactClient as jest.Mocked).getArtifact.mockImplementation( + async (id) => { + return ARTIFACTS_BY_ID[id]; + } + ); + const manifest = await manifestManager.getLastComputedManifest(); expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); @@ -418,8 +420,6 @@ describe('ManifestManager', () => { const context = buildManifestManagerContextMock({}); const manifestManager = new ManifestManager(context); - context.savedObjectsClient.delete = jest.fn().mockResolvedValue({}); - await expect( manifestManager.deleteArtifacts([ ARTIFACT_ID_EXCEPTIONS_MACOS, @@ -427,32 +427,27 @@ describe('ManifestManager', () => { ]) ).resolves.toStrictEqual([]); - expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + expect(context.artifactClient.deleteArtifact).toHaveBeenNthCalledWith( 1, - ArtifactConstants.SAVED_OBJECT_TYPE, ARTIFACT_ID_EXCEPTIONS_MACOS ); - expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + expect(context.artifactClient.deleteArtifact).toHaveBeenNthCalledWith( 2, - ArtifactConstants.SAVED_OBJECT_TYPE, ARTIFACT_ID_EXCEPTIONS_WINDOWS ); }); test('Returns errors for partial failures', async () => { const context = buildManifestManagerContextMock({}); + const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); const error = new Error(); - context.savedObjectsClient.delete = jest - .fn() - .mockImplementation(async (type: string, id: string) => { - if (id === ARTIFACT_ID_EXCEPTIONS_WINDOWS) { - throw error; - } else { - return {}; - } - }); + artifactClient.deleteArtifact.mockImplementation(async (id) => { + if (id === ARTIFACT_ID_EXCEPTIONS_WINDOWS) { + throw error; + } + }); await expect( manifestManager.deleteArtifacts([ @@ -461,46 +456,35 @@ describe('ManifestManager', () => { ]) ).resolves.toStrictEqual([error]); - expect(context.savedObjectsClient.delete).toHaveBeenCalledTimes(2); - expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + expect(artifactClient.deleteArtifact).toHaveBeenCalledTimes(2); + expect(artifactClient.deleteArtifact).toHaveBeenNthCalledWith( 1, - ArtifactConstants.SAVED_OBJECT_TYPE, ARTIFACT_ID_EXCEPTIONS_MACOS ); - expect(context.savedObjectsClient.delete).toHaveBeenNthCalledWith( + expect(artifactClient.deleteArtifact).toHaveBeenNthCalledWith( 2, - ArtifactConstants.SAVED_OBJECT_TYPE, ARTIFACT_ID_EXCEPTIONS_WINDOWS ); }); }); describe('pushArtifacts', () => { - test('Successfully invokes saved objects client and stores in the cache', async () => { + test('Successfully invokes artifactClient and stores in the cache', async () => { const context = buildManifestManagerContextMock({}); + const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); - context.savedObjectsClient.create = jest - .fn() - .mockImplementation((type: string, artifact: InternalArtifactCompleteSchema) => artifact); - await expect( manifestManager.pushArtifacts([ARTIFACT_EXCEPTIONS_MACOS, ARTIFACT_EXCEPTIONS_WINDOWS]) ).resolves.toStrictEqual([]); - expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(2); - expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( - 1, - ArtifactConstants.SAVED_OBJECT_TYPE, - { ...ARTIFACT_EXCEPTIONS_MACOS, created: expect.anything() }, - { id: ARTIFACT_ID_EXCEPTIONS_MACOS } - ); - expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( - 2, - ArtifactConstants.SAVED_OBJECT_TYPE, - { ...ARTIFACT_EXCEPTIONS_WINDOWS, created: expect.anything() }, - { id: ARTIFACT_ID_EXCEPTIONS_WINDOWS } - ); + expect(artifactClient.createArtifact).toHaveBeenCalledTimes(2); + expect(artifactClient.createArtifact).toHaveBeenNthCalledWith(1, { + ...ARTIFACT_EXCEPTIONS_MACOS, + }); + expect(artifactClient.createArtifact).toHaveBeenNthCalledWith(2, { + ...ARTIFACT_EXCEPTIONS_WINDOWS, + }); expect( await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!) ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_MACOS)); @@ -511,19 +495,20 @@ describe('ManifestManager', () => { test('Returns errors for partial failures', async () => { const context = buildManifestManagerContextMock({}); + const artifactClient = context.artifactClient as jest.Mocked; const manifestManager = new ManifestManager(context); const error = new Error(); const { body, ...incompleteArtifact } = ARTIFACT_TRUSTED_APPS_MACOS; - context.savedObjectsClient.create = jest - .fn() - .mockImplementation(async (type: string, artifact: InternalArtifactCompleteSchema) => { + artifactClient.createArtifact.mockImplementation( + async (artifact: InternalArtifactCompleteSchema) => { if (getArtifactId(artifact) === ARTIFACT_ID_EXCEPTIONS_WINDOWS) { throw error; } else { return artifact; } - }); + } + ); await expect( manifestManager.pushArtifacts([ @@ -536,45 +521,15 @@ describe('ManifestManager', () => { new Error(`Incomplete artifact: ${ARTIFACT_ID_TRUSTED_APPS_MACOS}`), ]); - expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(2); - expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( - 1, - ArtifactConstants.SAVED_OBJECT_TYPE, - { ...ARTIFACT_EXCEPTIONS_MACOS, created: expect.anything() }, - { id: ARTIFACT_ID_EXCEPTIONS_MACOS } - ); + expect(artifactClient.createArtifact).toHaveBeenCalledTimes(2); + expect(artifactClient.createArtifact).toHaveBeenNthCalledWith(1, { + ...ARTIFACT_EXCEPTIONS_MACOS, + }); expect( await uncompressData(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))!) ).toStrictEqual(await uncompressArtifact(ARTIFACT_EXCEPTIONS_MACOS)); expect(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_WINDOWS))).toBeUndefined(); }); - - test('Tolerates saved objects client conflict', async () => { - const context = buildManifestManagerContextMock({}); - const manifestManager = new ManifestManager(context); - - context.savedObjectsClient.create = jest - .fn() - .mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError( - ArtifactConstants.SAVED_OBJECT_TYPE, - ARTIFACT_ID_EXCEPTIONS_MACOS - ) - ); - - await expect( - manifestManager.pushArtifacts([ARTIFACT_EXCEPTIONS_MACOS]) - ).resolves.toStrictEqual([]); - - expect(context.savedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(context.savedObjectsClient.create).toHaveBeenNthCalledWith( - 1, - ArtifactConstants.SAVED_OBJECT_TYPE, - { ...ARTIFACT_EXCEPTIONS_MACOS, created: expect.anything() }, - { id: ARTIFACT_ID_EXCEPTIONS_MACOS } - ); - expect(context.cache.get(getArtifactId(ARTIFACT_EXCEPTIONS_MACOS))).toBeUndefined(); - }); }); describe('commit', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index e219da38931da..9ed17686fd2bc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -32,7 +32,7 @@ import { InternalArtifactCompleteSchema, internalArtifactCompleteSchema, } from '../../../schemas/artifacts'; -import { ArtifactClient } from '../artifact_client'; +import { EndpointArtifactClientInterface } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; interface ArtifactsBuildResult { @@ -76,7 +76,7 @@ const iterateAllListItems = async ( export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; - artifactClient: ArtifactClient; + artifactClient: EndpointArtifactClientInterface; exceptionListClient: ExceptionListClient; packagePolicyService: PackagePolicyServiceInterface; logger: Logger; @@ -92,7 +92,7 @@ const manifestsEqual = (manifest1: ManifestSchema, manifest2: ManifestSchema) => isEqual(new Set(getArtifactIds(manifest1)), new Set(getArtifactIds(manifest2))); export class ManifestManager { - protected artifactClient: ArtifactClient; + protected artifactClient: EndpointArtifactClientInterface; protected exceptionListClient: ExceptionListClient; protected packagePolicyService: PackagePolicyServiceInterface; protected savedObjectsClient: SavedObjectsClientContract; @@ -290,10 +290,13 @@ export class ManifestManager { ); for (const entry of manifestSo.attributes.artifacts) { - manifest.addEntry( - (await this.artifactClient.getArtifact(entry.artifactId)).attributes, - entry.policyId - ); + const artifact = await this.artifactClient.getArtifact(entry.artifactId); + + if (!artifact) { + throw new Error(`artifact id [${entry.artifactId}] not found!`); + } + + manifest.addEntry(artifact, entry.policyId); } return manifest; @@ -462,7 +465,7 @@ export class ManifestManager { }); } - public getArtifactsClient(): ArtifactClient { + public getArtifactsClient(): EndpointArtifactClientInterface { return this.artifactClient; } } diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts index b317b91b0ccea..944707d2afb28 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/install_prepackaged_rules.ts @@ -12,7 +12,7 @@ import { SecurityPluginSetup } from '../../../../security/server'; import { AppClientFactory } from '../../client'; import { createDetectionIndex } from '../../lib/detection_engine/routes/index/create_index_route'; import { createPrepackagedRules } from '../../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; -import { buildFrameworkRequest } from '../../lib/timeline/routes/utils/common'; +import { buildFrameworkRequest } from '../../lib/timeline/utils/common'; export interface InstallPrepackagedRulesProps { logger: Logger; diff --git a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts index 853b1738c4b14..383522bc06bdd 100644 --- a/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/note/resolvers.ts @@ -7,7 +7,7 @@ import { AppResolverWithFields, AppResolverOf } from '../../lib/framework'; import { MutationResolvers, QueryResolvers } from '../types'; -import { Note } from '../../lib/note/saved_object'; +import { Notes } from '../../lib/timeline/saved_object/notes'; export type QueryNoteResolver = AppResolverOf; @@ -29,7 +29,7 @@ export type MutationDeleteNoteResolver = AppResolverOf; interface NoteResolversDeps { - note: Note; + note: Notes; } export const createNoteResolvers = ( diff --git a/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts index 7a8ffb313d5c5..de1a40d9118c7 100644 --- a/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/pinned_event/resolvers.ts @@ -7,7 +7,7 @@ import { AppResolverOf } from '../../lib/framework'; import { MutationResolvers, QueryResolvers } from '../types'; -import { PinnedEvent } from '../../lib/pinned_event/saved_object'; +import { PinnedEvent } from '../../lib/timeline/saved_object/pinned_events'; export type QueryAllPinnedEventsByTimelineIdResolver = AppResolverOf; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts index 40264694d2f24..8aa08eda95923 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts @@ -7,7 +7,7 @@ import { AppResolverWithFields, AppResolverOf } from '../../lib/framework'; import { MutationResolvers, QueryResolvers } from '../types'; -import { Timeline } from '../../lib/timeline/saved_object'; +import { Timeline } from '../../lib/timeline/saved_object/timelines'; import { TimelineType } from '../../../common/types/timeline'; export type QueryTimelineResolver = AppResolverOf; diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index f5051bd700f00..5c83f70fdb10b 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -16,9 +16,7 @@ import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status'; import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; -import * as note from '../note/saved_object'; -import * as pinnedEvent from '../pinned_event/saved_object'; -import * as timeline from '../timeline/saved_object'; +import { note, pinnedEvent, timeline } from '../timeline/saved_object'; import { EndpointAppContext } from '../../endpoint/types'; export function compose( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index c6f432a28aee4..326d5777543be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -21,7 +21,7 @@ import ecsMapping from './ecs_mapping.json'; incremented by 10 in order to add "room" for the aforementioned patch release */ -export const SIGNALS_TEMPLATE_VERSION = 25; +export const SIGNALS_TEMPLATE_VERSION = 26; export const MIN_EQL_RULE_INDEX_VERSION = 2; export const getSignalsTemplate = (index: string) => { @@ -45,6 +45,19 @@ export const getSignalsTemplate = (index: string) => { properties: { ...ecsMapping.mappings.properties, signal: signalsMapping.mappings.properties.signal, + threat: { + ...ecsMapping.mappings.properties.threat, + properties: { + ...ecsMapping.mappings.properties.threat.properties, + indicator: { + ...ecsMapping.mappings.properties.threat.properties.indicator, + properties: { + ...ecsMapping.mappings.properties.threat.properties.indicator.properties, + event: ecsMapping.mappings.properties.event, + }, + }, + }, + }, }, _meta: { version: SIGNALS_TEMPLATE_VERSION, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index cf4b0bcf6f2d9..1195f9e5e1e96 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -14,12 +14,12 @@ import { import { requestContextMock, serverMock, createMockConfig, mockGetCurrentUser } from '../__mocks__'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { SecurityPluginSetup } from '../../../../../../security/server'; -import { installPrepackagedTimelines } from '../../../timeline/routes/utils/install_prepacked_timelines'; import { addPrepackedRulesRoute, createPrepackagedRules } from './add_prepackaged_rules_route'; import { listMock } from '../../../../../../lists/server/mocks'; import { siemMock } from '../../../../mocks'; import { FrameworkRequest } from '../../../framework'; import { ExceptionListClient } from '../../../../../../lists/server'; +import { installPrepackagedTimelines } from '../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; @@ -59,7 +59,7 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); -jest.mock('../../../timeline/routes/utils/install_prepacked_timelines', () => { +jest.mock('../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines', () => { return { installPrepackagedTimelines: jest.fn().mockResolvedValue({ success: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index e7e571647cbe4..8a8d6925b0e80 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -22,8 +22,7 @@ import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constant import { ConfigType } from '../../../../config'; import { SetupPlugins } from '../../../../plugin'; -import { buildFrameworkRequest } from '../../../timeline/routes/utils/common'; -import { installPrepackagedTimelines } from '../../../timeline/routes/utils/install_prepacked_timelines'; +import { buildFrameworkRequest } from '../../../timeline/utils/common'; import { getIndexExists } from '../../index/get_index_exists'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; @@ -38,6 +37,7 @@ import { AlertsClient } from '../../../../../../alerting/server'; import { FrameworkRequest } from '../../../framework'; import { ExceptionListClient } from '../../../../../../lists/server'; +import { installPrepackagedTimelines } from '../../../timeline/routes/prepackaged_timelines/install_prepackaged_timelines'; export const addPrepackedRulesRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index 5b59060edf996..9e843d463ab3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -15,11 +15,11 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, createMockConfig } from '../__mocks__'; import { SecurityPluginSetup } from '../../../../../../security/server'; -import { checkTimelinesStatus } from '../../../timeline/routes/utils/check_timelines_status'; +import { checkTimelinesStatus } from '../../../timeline/utils/check_timelines_status'; import { mockCheckTimelinesStatusBeforeInstallResult, mockCheckTimelinesStatusAfterInstallResult, -} from '../../../timeline/routes/__mocks__/import_timelines'; +} from '../../../timeline/__mocks__/import_timelines'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -44,8 +44,10 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); -jest.mock('../../../timeline/routes/utils/check_timelines_status', () => { +jest.mock('../../../timeline/utils/check_timelines_status', () => { + const actual = jest.requireActual('../../../timeline/utils/check_timelines_status'); return { + ...actual, checkTimelinesStatus: jest.fn(), }; }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 6f439bf7394a5..c67f2cb6e9545 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -18,11 +18,13 @@ import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { buildFrameworkRequest } from '../../../timeline/routes/utils/common'; +import { buildFrameworkRequest } from '../../../timeline/utils/common'; import { ConfigType } from '../../../../config'; import { SetupPlugins } from '../../../../plugin'; -import { checkTimelinesStatus } from '../../../timeline/routes/utils/check_timelines_status'; -import { checkTimelineStatusRt } from '../../../timeline/routes/schemas/check_timelines_status_schema'; +import { + checkTimelinesStatus, + checkTimelineStatusRt, +} from '../../../timeline/utils/check_timelines_status'; export const getPrepackagedRulesStatusRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh index 3ddb438a9523f..559683c2dcce8 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/timelines/delete_all_timelines.sh @@ -9,7 +9,7 @@ set -e ./check_env_variables.sh -# Example: ./timelines/delete_all_timelines.sh +# Example: sh ./timelines/delete_all_timelines.sh curl -s -k \ -H "Content-Type: application/json" \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 3a6cbf5ccd34b..0c03c0837e8e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -73,11 +73,12 @@ export const buildBulkBody = ({ ...buildSignal([doc], rule), ...additionalSignalFields(doc), }; - // @ts-expect-error @elastic/elasticsearch _source is optional - delete doc._source.threshold_result; const event = buildEventTypeSignal(doc); + const { threshold_result: thresholdResult, ...filteredSource } = doc._source || { + threshold_result: null, + }; const signalHit: SignalHit = { - ...doc._source, + ...filteredSource, '@timestamp': new Date().toISOString(), event, signal, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 7b3ca099cc93c..7c80572f6b1ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -83,6 +83,7 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { + event: { dataset: 'abuse.ch', reference: 'https://test.com' }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), @@ -117,6 +118,16 @@ describe('buildMatchedIndicator', () => { expect(get(indicator, 'matched.atomic')).toEqual('domain_1'); }); + it('returns event values as a part of threat', () => { + const [indicator] = buildMatchedIndicator({ + queries, + threats, + indicatorPath, + }); + const expectedEvent = threats[0]._source!.event; + expect(get(indicator, 'event')).toEqual(expectedEvent); + }); + it('returns the _id of the matched indicator as matched.id', () => { const [indicator] = buildMatchedIndicator({ queries, @@ -162,12 +173,16 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { - threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + event: { reference: 'https://test.com' }, + threat: { + indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' }, + }, }, }), getThreatListItemMock({ _id: '456', _source: { + event: { reference: 'https://test2.com' }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), @@ -205,6 +220,10 @@ describe('buildMatchedIndicator', () => { }, other: 'other_1', type: 'type_1', + event: { + reference: 'https://test.com', + dataset: 'abuse.ch', + }, }, ]); }); @@ -214,6 +233,9 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { + event: { + reference: 'https://test3.com', + }, 'threat.indicator.domain': 'domain_1', custom: { indicator: { @@ -244,6 +266,9 @@ describe('buildMatchedIndicator', () => { type: 'indicator_type', }, type: 'indicator_type', + event: { + reference: 'https://test3.com', + }, }, ]); }); @@ -307,6 +332,9 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { + event: { + reference: 'https://test4.com', + }, threat: { indicator: [ { domain: 'foo', type: 'first' }, @@ -334,6 +362,9 @@ describe('buildMatchedIndicator', () => { type: 'first', }, type: 'first', + event: { + reference: 'https://test4.com', + }, }, ]); }); @@ -392,6 +423,9 @@ describe('enrichSignalThreatMatches', () => { getThreatListItemMock({ _id: '123', _source: { + event: { + category: 'malware', + }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), @@ -419,7 +453,11 @@ describe('enrichSignalThreatMatches', () => { it('preserves existing threat.indicator objects on signals', async () => { const signalHit = getSignalHitMock({ - _source: { '@timestamp': 'mocked', threat: { indicator: [{ existing: 'indicator' }] } }, + _source: { + '@timestamp': 'mocked', + event: { category: 'malware' }, + threat: { indicator: [{ existing: 'indicator' }] }, + }, matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); @@ -444,6 +482,9 @@ describe('enrichSignalThreatMatches', () => { }, other: 'other_1', type: 'type_1', + event: { + category: 'malware', + }, }, ]); }); @@ -477,7 +518,11 @@ describe('enrichSignalThreatMatches', () => { it('preserves an existing threat.indicator object on signals', async () => { const signalHit = getSignalHitMock({ - _source: { '@timestamp': 'mocked', threat: { indicator: { existing: 'indicator' } } }, + _source: { + '@timestamp': 'mocked', + event: { category: 'virus' }, + threat: { indicator: { existing: 'indicator' } }, + }, matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); @@ -502,6 +547,9 @@ describe('enrichSignalThreatMatches', () => { }, other: 'other_1', type: 'type_1', + event: { + category: 'malware', + }, }, ]); }); @@ -573,12 +621,14 @@ describe('enrichSignalThreatMatches', () => { getThreatListItemMock({ _id: '123', _source: { + event: { category: 'threat' }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), getThreatListItemMock({ _id: '456', _source: { + event: { category: 'bad' }, threat: { indicator: { domain: 'domain_2', other: 'other_2', type: 'type_2' } }, }, }), @@ -622,6 +672,9 @@ describe('enrichSignalThreatMatches', () => { field: 'event.field', type: 'type_1', }, + event: { + category: 'threat', + }, other: 'other_1', type: 'type_1', }, @@ -634,6 +687,9 @@ describe('enrichSignalThreatMatches', () => { field: 'event.other', type: 'type_2', }, + event: { + category: 'bad', + }, other: 'other_2', type: 'type_2', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index 83a3ce8cb773f..c26f03d1dd480 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -57,9 +57,11 @@ export const buildMatchedIndicator = ({ } const atomic = get(matchedThreat?._source, query.value) as unknown; const type = get(indicator, 'type') as unknown; + const event = get(matchedThreat?._source, 'event') as unknown; return { ...indicator, + event, matched: { atomic, field: query.field, id: query.id, index: query.index, type }, }; }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index d5edd4678a9a2..b32d2a6542f4a 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -41,6 +41,7 @@ describe('TelemetryEventsSender', () => { }, file: { size: 3, + created: 0, path: 'X', test: 'me', another: 'nope', @@ -66,6 +67,20 @@ describe('TelemetryEventsSender', () => { }, something_else: 'nope', }, + process: { + name: 'foo.exe', + nope: 'nope', + executable: null, // null fields are never allowlisted + }, + Target: { + process: { + name: 'bar.exe', + nope: 'nope', + thread: { + id: 1234, + }, + }, + }, }, ]; @@ -85,6 +100,7 @@ describe('TelemetryEventsSender', () => { }, file: { size: 3, + created: 0, path: 'X', Ext: { code_signature: { @@ -106,6 +122,17 @@ describe('TelemetryEventsSender', () => { name: 'windows', }, }, + process: { + name: 'foo.exe', + }, + Target: { + process: { + name: 'bar.exe', + thread: { + id: 1234, + }, + }, + }, }, ]); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 114cf5d2d3425..7d723c578e3d0 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -293,10 +293,46 @@ interface AllowlistFields { [key: string]: boolean | AllowlistFields; } +// Allow list process fields within events. This includes "process" and "Target.process".' +/* eslint-disable @typescript-eslint/naming-convention */ +const allowlistProcessFields: AllowlistFields = { + name: true, + executable: true, + command_line: true, + hash: true, + pid: true, + uptime: true, + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + parent: { + name: true, + executable: true, + command_line: true, + hash: true, + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, + }, + }, + uptime: true, + pid: true, + ppid: true, + }, + thread: true, +}; + // Allow list for the data we include in the events. True means that it is deep-cloned // blindly. Object contents means that we only copy the fields that appear explicitly in // the sub-object. -/* eslint-disable @typescript-eslint/naming-convention */ const allowlistEventFields: AllowlistFields = { '@timestamp': true, agent: true, @@ -332,127 +368,9 @@ const allowlistEventFields: AllowlistFields = { host: { os: true, }, - process: { - name: true, - executable: true, - command_line: true, - hash: true, - pid: true, - uptime: true, - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, - }, - parent: { - name: true, - executable: true, - command_line: true, - hash: true, - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, - }, - uptime: true, - pid: true, - ppid: true, - }, - token: { - integrity_level_name: true, - }, - thread: true, - }, + process: allowlistProcessFields, Target: { - process: { - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, - }, - parent: { - process: { - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, - }, - }, - }, - thread: { - Ext: { - call_stack: true, - start_address: true, - start_address_allocation_offset: true, - start_address_bytes: true, - start_address_bytes_disasm: true, - start_address_bytes_disasm_hash: true, - start_address_details: { - allocation_base: true, - allocation_protection: true, - allocation_size: true, - allocation_type: true, - bytes_address: true, - bytes_allocation_offset: true, - bytes_compressed: true, - bytes_compressed_present: true, - mapped_pe: { - Ext: { - code_signature: { - status: true, - subject_name: true, - trusted: true, - }, - legal_copyright: true, - product_version: true, - }, - company: true, - description: true, - file_version: true, - imphash: true, - original_file_name: true, - product: true, - }, - mapped_pe_path: true, - memory_pe: { - Ext: { - code_signature: { - status: true, - subject_name: true, - trusted: true, - }, - legal_copyright: true, - product_version: true, - }, - company: true, - description: true, - file_version: true, - imphash: true, - original_file_name: true, - product: true, - }, - memory_pe_detected: true, - region_base: true, - region_protection: true, - region_size: true, - region_state: true, - strings: true, - }, - }, - }, - }, + process: allowlistProcessFields, }, }; @@ -462,7 +380,7 @@ export function copyAllowlistedFields( ): TelemetryEvent { return Object.entries(allowlist).reduce((newEvent, [allowKey, allowValue]) => { const eventValue = event[allowKey]; - if (eventValue) { + if (eventValue !== null && eventValue !== undefined) { if (allowValue === true) { return { ...newEvent, [allowKey]: eventValue }; } else if (typeof allowValue === 'object' && typeof eventValue === 'object') { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/create_timelines.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/create_timelines.ts rename to x-pack/plugins/security_solution/server/lib/timeline/__mocks__/create_timelines.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/import_timelines.ts similarity index 99% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts rename to x-pack/plugins/security_solution/server/lib/timeline/__mocks__/import_timelines.ts index 777c6a15f9d92..d7098556c9c3a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/import_timelines.ts @@ -6,7 +6,7 @@ */ import { omit } from 'lodash/fp'; -import { TimelineId, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/prepackaged_timelines.ndjson b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/prepackaged_timelines.ndjson similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/prepackaged_timelines.ndjson rename to x-pack/plugins/security_solution/server/lib/timeline/__mocks__/prepackaged_timelines.ndjson diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/request_responses.ts similarity index 95% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts rename to x-pack/plugins/security_solution/server/lib/timeline/__mocks__/request_responses.ts index e98a42a6a40f2..2cdcb92baed08 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/__mocks__/request_responses.ts @@ -14,14 +14,17 @@ import { TIMELINE_IMPORT_URL, TIMELINE_URL, TIMELINE_PREPACKAGED_URL, -} from '../../../../../common/constants'; -import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; +} from '../../../../common/constants'; +import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; -import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import { requestMock } from '../../detection_engine/routes/__mocks__'; + +import { + patchTimelineSchema, + createTimelineSchema, + GetTimelineByIdSchemaQuery, +} from '../schemas/timelines'; -import { updateTimelineSchema } from '../schemas/update_timelines_schema'; -import { createTimelineSchema } from '../schemas/create_timelines_schema'; -import { GetTimelineByIdSchemaQuery } from '../schemas/get_timeline_by_id_schema'; import { getReadables } from '../utils/common'; export const getExportTimelinesRequest = () => @@ -37,7 +40,7 @@ export const getExportTimelinesRequest = () => }); export const getImportTimelinesRequest = async (fileName?: string) => { - const dir = resolve(join(__dirname, '../../../detection_engine/rules/prepackaged_timelines')); + const dir = resolve(join(__dirname, '../../detection_engine/rules/prepackaged_timelines')); const file = fileName ?? 'index.ndjson'; const dataPath = path.join(dir, file); const readable = await getReadables(dataPath); @@ -147,7 +150,7 @@ export const getCreateTimelinesRequest = (mockBody: rt.TypeOf) => +export const getUpdateTimelinesRequest = (mockBody: rt.TypeOf) => requestMock.create({ method: 'patch', path: TIMELINE_URL, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts similarity index 85% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts index de83b9aa10426..1b230ad0446ce 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.test.ts @@ -5,21 +5,21 @@ * 2.0. */ -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; -import { TimelineType } from '../../../../common/types/timeline'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; +import { TimelineType } from '../../../../../../common/types/timeline'; import { serverMock, requestContextMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; +} from '../../../../detection_engine/routes/__mocks__'; -import { mockGetCurrentUser, mockGetDraftTimelineValue } from './__mocks__/import_timelines'; +import { mockGetCurrentUser, mockGetDraftTimelineValue } from '../../../__mocks__/import_timelines'; import { cleanDraftTimelinesRequest, createTimelineWithTimelineId, -} from './__mocks__/request_responses'; -import { draftTimelineDefaults } from '../default_timeline'; +} from '../../../__mocks__/request_responses'; +import { draftTimelineDefaults } from '../../../utils/default_timeline'; describe('clean draft timelines', () => { let server: ReturnType; @@ -55,7 +55,7 @@ describe('clean draft timelines', () => { mockPersistNote = jest.fn(); mockResetTimeline = jest.fn(); - jest.doMock('../saved_object', () => ({ + jest.doMock('../../../saved_object/timelines', () => ({ getTimeline: mockGetTimeline, getDraftTimeline: mockGetDraftTimeline, resetTimeline: mockResetTimeline, @@ -65,16 +65,15 @@ describe('clean draft timelines', () => { }), })); - jest.doMock('../../pinned_event/saved_object', () => ({ + jest.doMock('../../../saved_object/pinned_events', () => ({ persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, })); - jest.doMock('../../note/saved_object', () => ({ + jest.doMock('../../../saved_object/notes', () => ({ persistNote: mockPersistNote, })); - const cleanDraftTimelinesRoute = jest.requireActual('./clean_draft_timelines_route') - .cleanDraftTimelinesRoute; + const cleanDraftTimelinesRoute = jest.requireActual('./index').cleanDraftTimelinesRoute; cleanDraftTimelinesRoute(server.router, createMockConfig(), securitySetup); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts similarity index 75% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts index f7385f56b3244..113860f369f78 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts @@ -6,17 +6,22 @@ */ import uuid from 'uuid'; -import type { SecuritySolutionPluginRouter } from '../../../types'; -import { ConfigType } from '../../..'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; -import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; -import { buildFrameworkRequest } from './utils/common'; -import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; -import { getDraftTimeline, resetTimeline, getTimeline, persistTimeline } from '../saved_object'; -import { draftTimelineDefaults } from '../default_timeline'; -import { cleanDraftTimelineSchema } from './schemas/clean_draft_timelines_schema'; -import { TimelineType } from '../../../../common/types/timeline'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { ConfigType } from '../../../../..'; +import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; +import { buildFrameworkRequest } from '../../../utils/common'; +import { SetupPlugins } from '../../../../../plugin'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; +import { + getDraftTimeline, + resetTimeline, + getTimeline, + persistTimeline, +} from '../../../saved_object/timelines'; +import { draftTimelineDefaults } from '../../../utils/default_timeline'; +import { cleanDraftTimelineSchema } from '../../../schemas/draft_timelines'; +import { TimelineType } from '../../../../../../common/types/timeline'; export const cleanDraftTimelinesRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts similarity index 84% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts index 7bfdbef920ef7..7e3e2a23222c3 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.test.ts @@ -5,21 +5,21 @@ * 2.0. */ -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; -import { TimelineType } from '../../../../common/types/timeline'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; +import { TimelineType } from '../../../../../../common/types/timeline'; import { serverMock, requestContextMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; +} from '../../../../detection_engine/routes/__mocks__'; -import { mockGetCurrentUser, mockGetDraftTimelineValue } from './__mocks__/import_timelines'; +import { mockGetCurrentUser, mockGetDraftTimelineValue } from '../../../__mocks__/import_timelines'; import { getDraftTimelinesRequest, createTimelineWithTimelineId, -} from './__mocks__/request_responses'; -import { draftTimelineDefaults } from '../default_timeline'; +} from '../../../__mocks__/request_responses'; +import { draftTimelineDefaults } from '../../../utils/default_timeline'; describe('get draft timelines', () => { let server: ReturnType; @@ -57,7 +57,7 @@ describe('get draft timelines', () => { describe('Manipulate timeline', () => { describe('Create a new timeline', () => { beforeEach(async () => { - jest.doMock('../saved_object', () => ({ + jest.doMock('../../../saved_object/timelines', () => ({ getTimeline: mockGetTimeline, getDraftTimeline: mockGetDraftTimeline, persistTimeline: mockPersistTimeline.mockReturnValue({ @@ -66,16 +66,15 @@ describe('get draft timelines', () => { }), })); - jest.doMock('../../pinned_event/saved_object', () => ({ + jest.doMock('../../../saved_object/pinned_events', () => ({ persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, })); - jest.doMock('../../note/saved_object', () => ({ + jest.doMock('../../../saved_object/notes', () => ({ persistNote: mockPersistNote, })); - const getDraftTimelinesRoute = jest.requireActual('./get_draft_timelines_route') - .getDraftTimelinesRoute; + const getDraftTimelinesRoute = jest.requireActual('./index').getDraftTimelinesRoute; getDraftTimelinesRoute(server.router, createMockConfig(), securitySetup); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts similarity index 72% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts index 491650bae9605..f3f813ace411d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts @@ -5,16 +5,16 @@ * 2.0. */ -import type { SecuritySolutionPluginRouter } from '../../../types'; -import { ConfigType } from '../../..'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; -import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; -import { buildFrameworkRequest } from './utils/common'; -import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; -import { getDraftTimeline, persistTimeline } from '../saved_object'; -import { draftTimelineDefaults } from '../default_timeline'; -import { getDraftTimelineSchema } from './schemas/get_draft_timelines_schema'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { ConfigType } from '../../../../..'; +import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; +import { buildFrameworkRequest } from '../../../utils/common'; +import { SetupPlugins } from '../../../../../plugin'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; +import { getDraftTimeline, persistTimeline } from '../../../saved_object/timelines'; +import { draftTimelineDefaults } from '../../../utils/default_timeline'; +import { getDraftTimelineSchema } from '../../../schemas/draft_timelines'; export const getDraftTimelinesRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts similarity index 92% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts index cf025674e9e21..2b3eda31916a8 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.test.ts @@ -8,26 +8,26 @@ import { join, resolve } from 'path'; import { createPromiseFromStreams } from '@kbn/utils'; -import { SecurityPluginSetup } from '../../../../../../security/server'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; -import { FrameworkRequest } from '../../../framework'; +import { FrameworkRequest } from '../../../../framework'; import { createMockConfig, requestContextMock, mockGetCurrentUser, -} from '../../../detection_engine/routes/__mocks__'; +} from '../../../../detection_engine/routes/__mocks__'; import { addPrepackagedRulesRequest, getNonEmptyIndex, getFindResultWithSingleHit, -} from '../../../detection_engine/routes/__mocks__/request_responses'; +} from '../../../../detection_engine/routes/__mocks__/request_responses'; -import * as lib from './install_prepacked_timelines'; -import { importTimelines } from './import_timelines'; -import { buildFrameworkRequest } from './common'; -import { ImportTimelineResultSchema } from '../../../../../common/types/timeline'; +import * as lib from './helpers'; +import { importTimelines } from '../../timelines/import_timelines'; +import { buildFrameworkRequest } from '../../../utils/common'; +import { ImportTimelineResultSchema } from '../../../../../../common/types/timeline'; -jest.mock('./import_timelines'); +jest.mock('../../timelines/import_timelines'); describe('installPrepackagedTimelines', () => { let securitySetup: SecurityPluginSetup; @@ -36,7 +36,7 @@ describe('installPrepackagedTimelines', () => { const { clients, context } = requestContextMock.createTools(); const config = createMockConfig(); - const mockFilePath = '../__mocks__'; + const mockFilePath = '../../../__mocks__'; const mockFileName = 'prepackaged_timelines.ndjson'; beforeEach(async () => { @@ -50,7 +50,7 @@ describe('installPrepackagedTimelines', () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - jest.doMock('./install_prepacked_timelines', () => { + jest.doMock('./helpers', () => { return { ...lib, installPrepackagedTimelines: spyInstallPrepackagedTimelines, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.ts similarity index 78% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.ts index f402d2a4d7f03..c2ff89ee1d2ad 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/helpers.ts @@ -8,13 +8,13 @@ import path, { join, resolve } from 'path'; import { Readable } from 'stream'; -import { ImportTimelineResultSchema } from '../../../../../common/types/timeline'; +import { ImportTimelineResultSchema } from '../../../../../../common/types/timeline'; -import { FrameworkRequest } from '../../../framework'; +import { FrameworkRequest } from '../../../../framework'; -import { importTimelines } from './import_timelines'; +import { importTimelines } from '../../timelines/import_timelines'; -import { loadData, getReadables } from './common'; +import { loadData, getReadables } from '../../../utils/common'; export const installPrepackagedTimelines = async ( maxTimelineImportExportSize: number, @@ -25,7 +25,7 @@ export const installPrepackagedTimelines = async ( ): Promise => { let readStream; const dir = resolve( - join(__dirname, filePath ?? '../../../detection_engine/rules/prepackaged_timelines') + join(__dirname, filePath ?? '../../../../detection_engine/rules/prepackaged_timelines') ); const file = fileName ?? 'index.ndjson'; const dataPath = path.join(dir, file); @@ -44,7 +44,6 @@ export const installPrepackagedTimelines = async ( ], }; } - return loadData(readStream, (docs: T) => docs instanceof Readable ? importTimelines(docs, maxTimelineImportExportSize, frameworkRequest, isImmutable) diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts similarity index 80% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts index 6e26baa921643..427086f3c4b6c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.test.ts @@ -5,33 +5,37 @@ * 2.0. */ -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; import { serverMock, requestContextMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; +} from '../../../../detection_engine/routes/__mocks__'; import { mockGetCurrentUser, mockCheckTimelinesStatusBeforeInstallResult, mockCheckTimelinesStatusAfterInstallResult, -} from './__mocks__/import_timelines'; -import { installPrepackedTimelinesRequest } from './__mocks__/request_responses'; +} from '../../../__mocks__/import_timelines'; +import { installPrepackedTimelinesRequest } from '../../../__mocks__/request_responses'; -import { installPrepackagedTimelines } from './utils/install_prepacked_timelines'; -import { checkTimelinesStatus } from './utils/check_timelines_status'; +import { installPrepackagedTimelines } from './helpers'; +import { checkTimelinesStatus } from '../../../utils/check_timelines_status'; -import { installPrepackedTimelinesRoute } from './install_prepacked_timelines_route'; +import { installPrepackedTimelinesRoute } from '.'; -jest.mock('./utils/install_prepacked_timelines', () => ({ +jest.mock('./helpers', () => ({ installPrepackagedTimelines: jest.fn(), })); -jest.mock('./utils/check_timelines_status', () => ({ - checkTimelinesStatus: jest.fn(), -})); +jest.mock('../../../utils/check_timelines_status', () => { + const actual = jest.requireActual('../../../utils/check_timelines_status'); + return { + ...actual, + checkTimelinesStatus: jest.fn(), + }; +}); describe('installPrepackagedTimelines', () => { let server: ReturnType; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts similarity index 76% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts index 5366f5b286325..bb447948df24a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/install_prepacked_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts @@ -5,22 +5,23 @@ * 2.0. */ -import type { SecuritySolutionPluginRouter } from '../../../types'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { TIMELINE_PREPACKAGED_URL } from '../../../../common/constants'; +import { TIMELINE_PREPACKAGED_URL } from '../../../../../../common/constants'; -import { SetupPlugins } from '../../../plugin'; -import { ConfigType } from '../../../config'; -import { validate } from '../../../../common/validate'; +import { SetupPlugins } from '../../../../../plugin'; +import { ConfigType } from '../../../../../config'; +import { validate } from '../../../../../../common/validate'; -import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; +import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; -import { installPrepackagedTimelines } from './utils/install_prepacked_timelines'; +import { installPrepackagedTimelines } from './helpers'; -import { checkTimelinesStatus } from './utils/check_timelines_status'; +import { checkTimelinesStatus, checkTimelineStatusRt } from '../../../utils/check_timelines_status'; -import { checkTimelineStatusRt } from './schemas/check_timelines_status_schema'; -import { buildFrameworkRequest } from './utils/common'; +import { buildFrameworkRequest } from '../../../utils/common'; + +export { installPrepackagedTimelines } from './helpers'; export const installPrepackedTimelinesRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts deleted file mode 100644 index 8fd38ad80acd2..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/check_timelines_status_schema.ts +++ /dev/null @@ -1,20 +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 * as rt from 'io-ts'; -import { TimelineSavedToReturnObjectRuntimeType } from '../../../../../common/types/timeline'; - -import { ImportTimelinesSchemaRt } from './import_timelines_schema'; -import { unionWithNullType } from '../../../../../common/utility_types'; - -export const checkTimelineStatusRt = rt.type({ - timelinesToInstall: rt.array(unionWithNullType(ImportTimelinesSchemaRt)), - timelinesToUpdate: rt.array(unionWithNullType(ImportTimelinesSchemaRt)), - prepackagedTimelines: rt.array(unionWithNullType(TimelineSavedToReturnObjectRuntimeType)), -}); - -export type CheckTimelineStatusRt = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts similarity index 64% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts index d128b2e992f25..f5e5b7dfb8ae9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts @@ -5,15 +5,22 @@ * 2.0. */ -import * as module from './create_timelines'; -import { persistTimeline } from '../../saved_object'; -import { persistPinnedEventOnTimeline } from '../../../pinned_event/saved_object'; -import { persistNote, getNote } from '../../../note/saved_object'; -import { FrameworkRequest } from '../../../framework'; -import { SavedTimeline } from '../../../../../common/types/timeline'; -import { mockTemplate, mockTimeline } from '../__mocks__/create_timelines'; - -const frameworkRequest = {} as FrameworkRequest; +import * as module from './helpers'; +import { savePinnedEvents } from '../../../saved_object/pinned_events'; +import { getNote } from '../../../saved_object/notes'; +import { FrameworkRequest } from '../../../../framework'; +import { SavedTimeline } from '../../../../../../common/types/timeline'; +import { mockTemplate, mockTimeline } from '../../../__mocks__/create_timelines'; +import { buildFrameworkRequest } from '../../../utils/common'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; +import { requestContextMock } from '../../../../detection_engine/routes/__mocks__'; +import { + getCreateTimelinesRequest, + createTimelineWithoutTimelineId, +} from '../../../__mocks__/request_responses'; +import { persistTimeline } from '../../../saved_object/timelines'; +import { persistNotes } from '../../../saved_object/notes/persist_notes'; + const template = { ...mockTemplate } as SavedTimeline; const timeline = { ...mockTimeline } as SavedTimeline; const timelineSavedObjectId = null; @@ -24,7 +31,6 @@ const notes = [ ]; const existingNoteIds = undefined; const isImmutable = true; -const newTimelineSavedObjectId = 'eb2781c0-1df5-11eb-8589-2f13958b79f7'; jest.mock('moment', () => { const mockMoment = { @@ -38,7 +44,7 @@ jest.mock('moment', () => { return jest.fn().mockReturnValue(mockMoment); }); -jest.mock('../../saved_object', () => ({ +jest.mock('../../../saved_object/timelines', () => ({ persistTimeline: jest.fn().mockResolvedValue({ timeline: { savedObjectId: 'eb2781c0-1df5-11eb-8589-2f13958b79f7', @@ -47,16 +53,37 @@ jest.mock('../../saved_object', () => ({ }), })); -jest.mock('../../../pinned_event/saved_object', () => ({ - persistPinnedEventOnTimeline: jest.fn(), +jest.mock('../../../saved_object/pinned_events', () => ({ + savePinnedEvents: jest.fn(), })); -jest.mock('../../../note/saved_object', () => ({ +jest.mock('../../../saved_object/notes', () => ({ getNote: jest.fn(), persistNote: jest.fn(), })); +jest.mock('../../../saved_object/notes/persist_notes', () => ({ + persistNotes: jest.fn(), +})); + describe('createTimelines', () => { + let securitySetup: SecurityPluginSetup; + let frameworkRequest: FrameworkRequest; + + beforeAll(async () => { + securitySetup = ({ + authc: { + getCurrentUser: jest.fn(), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + const { context } = requestContextMock.createTools(); + const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId); + + frameworkRequest = await buildFrameworkRequest(context, securitySetup, mockRequest); + }); + describe('create timelines', () => { beforeAll(async () => { await module.createTimelines({ @@ -88,15 +115,19 @@ describe('createTimelines', () => { }); test('savePinnedEvents', () => { - expect((persistPinnedEventOnTimeline as jest.Mock).mock.calls[0][2]).toEqual('123'); + expect((savePinnedEvents as jest.Mock).mock.calls[0][2]).toEqual(['123']); }); - test('saveNotes', () => { - expect((persistNote as jest.Mock).mock.calls[0][3]).toEqual({ - eventId: undefined, - note: 'new note', - timelineId: newTimelineSavedObjectId, - }); + test('persistNotes', () => { + expect((persistNotes as jest.Mock).mock.calls[0][4]).toEqual([ + { + created: 1603885051655, + createdBy: 'elastic', + note: 'new note', + noteId: 'abc', + timelineId: '', + }, + ]); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts new file mode 100644 index 0000000000000..626f3cbed5b77 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts @@ -0,0 +1,89 @@ +/* + * Copyright 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 { isEmpty } from 'lodash/fp'; + +import moment from 'moment'; +import { timeline as timelineLib, pinnedEvent as pinnedEventLib } from '../../../saved_object'; +import { FrameworkRequest } from '../../../../framework'; +import { SavedTimeline } from '../../../../../../common/types/timeline'; +import { NoteResult, ResponseTimeline } from '../../../../../graphql/types'; +import { persistNotes } from '../../../saved_object/notes/persist_notes'; + +interface CreateTimelineProps { + frameworkRequest: FrameworkRequest; + timeline: SavedTimeline; + timelineSavedObjectId?: string | null; + timelineVersion?: string | null; + overrideNotesOwner?: boolean; + pinnedEventIds?: string[] | null; + notes?: NoteResult[]; + existingNoteIds?: string[]; + isImmutable?: boolean; +} + +/** allow overrideNotesOwner means overriding by current username, + * disallow overrideNotesOwner means keep the original username. + * overrideNotesOwner = false only happens when import timeline templates, + * as we want to keep the original creator for notes + **/ +export const createTimelines = async ({ + frameworkRequest, + timeline, + timelineSavedObjectId = null, + timelineVersion = null, + pinnedEventIds = null, + notes = [], + existingNoteIds = [], + isImmutable, + overrideNotesOwner = true, +}: CreateTimelineProps): Promise => { + const timerangeStart = isImmutable + ? moment().subtract(24, 'hours').toISOString() + : timeline.dateRange?.start; + const timerangeEnd = isImmutable ? moment().toISOString() : timeline.dateRange?.end; + const responseTimeline = await timelineLib.persistTimeline( + frameworkRequest, + timelineSavedObjectId, + timelineVersion, + { ...timeline, dateRange: { start: timerangeStart, end: timerangeEnd } }, + isImmutable + ); + const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; + const newTimelineVersion = responseTimeline.timeline.version; + + let myPromises: unknown[] = []; + if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) { + myPromises = [ + ...myPromises, + pinnedEventLib.savePinnedEvents( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + pinnedEventIds + ), + ]; + } + if (!isEmpty(notes)) { + myPromises = [ + ...myPromises, + persistNotes( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + newTimelineVersion, + existingNoteIds, + notes, + overrideNotesOwner + ), + ]; + } + + if (myPromises.length > 0) { + await Promise.all(myPromises); + } + + return responseTimeline; +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts similarity index 80% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts index 0b029e4137825..995d8549fff94 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.test.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; import { serverMock, requestContextMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; +} from '../../../../detection_engine/routes/__mocks__'; import { mockGetCurrentUser, mockGetTimelineValue, mockGetTemplateTimelineValue, -} from './__mocks__/import_timelines'; +} from '../../../__mocks__/import_timelines'; import { getCreateTimelinesRequest, inputTimeline, @@ -26,11 +26,11 @@ import { createTemplateTimelineWithoutTimelineId, createTemplateTimelineWithTimelineId, updateTemplateTimelineWithTimelineId, -} from './__mocks__/request_responses'; +} from '../../../__mocks__/request_responses'; import { CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, CREATE_TIMELINE_ERROR_MESSAGE, -} from './utils/failure_cases'; +} from '../../../utils/failure_cases'; describe('create timelines', () => { let server: ReturnType; @@ -68,7 +68,7 @@ describe('create timelines', () => { describe('Manipulate timeline', () => { describe('Create a new timeline', () => { beforeEach(async () => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { getTimeline: mockGetTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ @@ -77,20 +77,19 @@ describe('create timelines', () => { }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const createTimelinesRoute = jest.requireActual('./create_timelines_route') - .createTimelinesRoute; + const createTimelinesRoute = jest.requireActual('./index').createTimelinesRoute; createTimelinesRoute(server.router, createMockConfig(), securitySetup); const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId); @@ -132,27 +131,26 @@ describe('create timelines', () => { describe('Import a timeline already exist', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTimelineValue), persistTimeline: mockPersistTimeline, }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const createTimelinesRoute = jest.requireActual('./create_timelines_route') - .createTimelinesRoute; + const createTimelinesRoute = jest.requireActual('./index').createTimelinesRoute; createTimelinesRoute(server.router, createMockConfig(), securitySetup); }); @@ -172,29 +170,28 @@ describe('create timelines', () => { describe('Manipulate timeline template', () => { describe('Create a new timeline template', () => { beforeEach(async () => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: createTemplateTimelineWithTimelineId, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const createTimelinesRoute = jest.requireActual('./create_timelines_route') - .createTimelinesRoute; + const createTimelinesRoute = jest.requireActual('./index').createTimelinesRoute; createTimelinesRoute(server.router, createMockConfig(), securitySetup); const mockRequest = getCreateTimelinesRequest(createTemplateTimelineWithoutTimelineId); @@ -238,30 +235,29 @@ describe('create timelines', () => { describe('Create a timeline template already exist', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [mockGetTemplateTimelineValue], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue( + mockGetTemplateTimelineValue + ), persistTimeline: mockPersistTimeline, }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const createTimelinesRoute = jest.requireActual('./create_timelines_route') - .createTimelinesRoute; + const createTimelinesRoute = jest.requireActual('./index').createTimelinesRoute; createTimelinesRoute(server.router, createMockConfig(), securitySetup); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts similarity index 78% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts index 4abfe4f7f7145..f35ddf1a76c7d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts @@ -5,24 +5,26 @@ * 2.0. */ -import type { SecuritySolutionPluginRouter } from '../../../types'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { TIMELINE_URL } from '../../../../common/constants'; +import { TIMELINE_URL } from '../../../../../../common/constants'; -import { ConfigType } from '../../..'; -import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; +import { ConfigType } from '../../../../..'; +import { SetupPlugins } from '../../../../../plugin'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; +import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; -import { createTimelineSchema } from './schemas/create_timelines_schema'; +import { createTimelineSchema } from '../../../schemas/timelines'; import { buildFrameworkRequest, CompareTimelinesStatus, TimelineStatusActions, -} from './utils/common'; -import { createTimelines } from './utils/create_timelines'; -import { DEFAULT_ERROR } from './utils/failure_cases'; +} from '../../../utils/common'; +import { DEFAULT_ERROR } from '../../../utils/failure_cases'; +import { createTimelines } from './helpers'; + +export * from './helpers'; export const createTimelinesRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/helpers.ts similarity index 83% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/helpers.ts index fd6f262d6fff6..a33b8be0c2f31 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/helpers.ts @@ -11,17 +11,17 @@ import { ExportedTimelines, ExportedNotes, ExportTimelineNotFoundError, -} from '../../../../../common/types/timeline'; -import { NoteSavedObject } from '../../../../../common/types/timeline/note'; -import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event'; +} from '../../../../../../common/types/timeline'; +import { NoteSavedObject } from '../../../../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../../../../common/types/timeline/pinned_event'; -import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; +import { transformDataToNdjson } from '../../../../../utils/read_stream/create_stream_from_ndjson'; -import { FrameworkRequest } from '../../../framework'; -import * as noteLib from '../../../note/saved_object'; -import * as pinnedEventLib from '../../../pinned_event/saved_object'; +import { FrameworkRequest } from '../../../../framework'; +import * as noteLib from '../../../saved_object/notes'; +import * as pinnedEventLib from '../../../saved_object/pinned_events'; -import { getSelectedTimelines } from '../../saved_object'; +import { getSelectedTimelines } from '../../../saved_object/timelines'; const getGlobalEventNotesByTimelineId = (currentNotes: NoteSavedObject[]): ExportedNotes => { const initialNotes: ExportedNotes = { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts similarity index 79% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts index a021ca5cf1c48..da6d0059d5738 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.test.ts @@ -11,35 +11,35 @@ import { mockTimelinesSavedObjects, mockPinnedEvents, getExportTimelinesRequest, -} from './__mocks__/request_responses'; -import { exportTimelinesRoute } from './export_timelines_route'; +} from '../../../__mocks__/request_responses'; +import { exportTimelinesRoute } from '.'; import { serverMock, requestContextMock, requestMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; -import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { convertSavedObjectToSavedNote } from '../../note/saved_object'; -import { convertSavedObjectToSavedPinnedEvent } from '../../pinned_event/saved_object'; -import { convertSavedObjectToSavedTimeline } from '../convert_saved_object_to_savedtimeline'; -import { mockGetCurrentUser } from './__mocks__/import_timelines'; -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; +} from '../../../../detection_engine/routes/__mocks__'; +import { TIMELINE_EXPORT_URL } from '../../../../../../common/constants'; +import { convertSavedObjectToSavedNote } from '../../../saved_object/notes/saved_object'; +import { convertSavedObjectToSavedPinnedEvent } from '../../../saved_object/pinned_events'; +import { convertSavedObjectToSavedTimeline } from '../../../saved_object/timelines/convert_saved_object_to_savedtimeline'; +import { mockGetCurrentUser } from '../../../__mocks__/import_timelines'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; -jest.mock('../convert_saved_object_to_savedtimeline', () => { +jest.mock('../../../saved_object/timelines/convert_saved_object_to_savedtimeline', () => { return { convertSavedObjectToSavedTimeline: jest.fn(), }; }); -jest.mock('../../note/saved_object', () => { +jest.mock('../../../saved_object/notes/saved_object', () => { return { convertSavedObjectToSavedNote: jest.fn(), getNotesByTimelineId: jest.fn().mockReturnValue([]), }; }); -jest.mock('../../pinned_event/saved_object', () => { +jest.mock('../../../saved_object/pinned_events', () => { return { convertSavedObjectToSavedPinnedEvent: jest.fn(), getAllPinnedEventsByTimelineId: jest.fn().mockReturnValue([]), diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts similarity index 75% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts index 19ee945836da5..9e1eabc4450bd 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts @@ -5,19 +5,22 @@ * 2.0. */ -import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import type { SecuritySolutionPluginRouter } from '../../../types'; -import { ConfigType } from '../../../config'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; +import { TIMELINE_EXPORT_URL } from '../../../../../../common/constants'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; +import { ConfigType } from '../../../../../config'; +import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; -import { getExportTimelineByObjectIds } from './utils/export_timelines'; import { exportTimelinesQuerySchema, exportTimelinesRequestBodySchema, -} from './schemas/export_timelines_schema'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; -import { buildFrameworkRequest } from './utils/common'; -import { SetupPlugins } from '../../../plugin'; +} from '../../../schemas/timelines'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; +import { buildFrameworkRequest } from '../../../utils/common'; +import { SetupPlugins } from '../../../../../plugin'; + +import { getExportTimelineByObjectIds } from './helpers'; + +export * from './helpers'; export const exportTimelinesRoute = ( router: SecuritySolutionPluginRouter, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts similarity index 62% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts index 35db918f96c00..8c559daa93da9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.test.ts @@ -5,28 +5,28 @@ * 2.0. */ -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; import { serverMock, requestContextMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; -import { getAllTimeline } from '../saved_object'; - -import { mockGetCurrentUser } from './__mocks__/import_timelines'; -import { getTimelineRequest } from './__mocks__/request_responses'; +} from '../../../../detection_engine/routes/__mocks__'; +import { + getTimelineOrNull, + getTimelineTemplateOrNull, + getAllTimeline, +} from '../../../saved_object/timelines'; -import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { getTimelineRoute } from './get_timeline_route'; +import { mockGetCurrentUser } from '../../../__mocks__/import_timelines'; +import { getTimelineRequest } from '../../../__mocks__/request_responses'; -jest.mock('./utils/create_timelines', () => ({ - getTimeline: jest.fn(), - getTemplateTimeline: jest.fn(), -})); +import { getTimelineRoute } from '.'; -jest.mock('../saved_object', () => ({ +jest.mock('../../../saved_object/timelines', () => ({ getAllTimeline: jest.fn(), + getTimelineOrNull: jest.fn(), + getTimelineTemplateOrNull: jest.fn(), })); describe('get timeline', () => { @@ -51,19 +51,19 @@ describe('get timeline', () => { getTimelineRoute(server.router, createMockConfig(), securitySetup); }); - test('should call getTemplateTimeline if templateTimelineId is given', async () => { + test('should call getTimelineTemplateOrNull if templateTimelineId is given', async () => { const templateTimelineId = '123'; await server.inject(getTimelineRequest({ template_timeline_id: templateTimelineId }), context); - expect((getTemplateTimeline as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); + expect((getTimelineTemplateOrNull as jest.Mock).mock.calls[0][1]).toEqual(templateTimelineId); }); - test('should call getTimeline if id is given', async () => { + test('should call getTimelineOrNull if id is given', async () => { const id = '456'; await server.inject(getTimelineRequest({ id }), context); - expect((getTimeline as jest.Mock).mock.calls[0][1]).toEqual(id); + expect((getTimelineOrNull as jest.Mock).mock.calls[0][1]).toEqual(id); }); test('should call getAllTimeline if nither templateTimelineId nor id is given', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts similarity index 67% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts index 3cd43af442eb6..f49110d105765 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts @@ -5,21 +5,24 @@ * 2.0. */ -import type { SecuritySolutionPluginRouter } from '../../../types'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { TIMELINE_URL } from '../../../../common/constants'; +import { TIMELINE_URL } from '../../../../../../common/constants'; -import { ConfigType } from '../../..'; -import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; +import { ConfigType } from '../../../../..'; +import { SetupPlugins } from '../../../../../plugin'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; -import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; +import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; -import { buildFrameworkRequest } from './utils/common'; -import { getTimelineByIdSchemaQuery } from './schemas/get_timeline_by_id_schema'; -import { getTimeline, getTemplateTimeline } from './utils/create_timelines'; -import { getAllTimeline } from '../saved_object'; -import { TimelineStatus } from '../../../../common/types/timeline'; +import { buildFrameworkRequest } from '../../../utils/common'; +import { getTimelineByIdSchemaQuery } from '../../../schemas/timelines'; +import { + getTimelineTemplateOrNull, + getTimelineOrNull, + getAllTimeline, +} from '../../../saved_object/timelines'; +import { TimelineStatus } from '../../../../../../common/types/timeline'; export const getTimelineRoute = ( router: SecuritySolutionPluginRouter, @@ -41,9 +44,9 @@ export const getTimelineRoute = ( const { template_timeline_id: templateTimelineId, id } = query; let res = null; if (templateTimelineId != null && id == null) { - res = await getTemplateTimeline(frameworkRequest, templateTimelineId); + res = await getTimelineTemplateOrNull(frameworkRequest, templateTimelineId); } else if (templateTimelineId == null && id != null) { - res = await getTimeline(frameworkRequest, id); + res = await getTimelineOrNull(frameworkRequest, id); } else if (templateTimelineId == null && id == null) { const tempResult = await getAllTimeline( frameworkRequest, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts similarity index 84% rename from x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts index 118f732735526..1184629e47e87 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts @@ -16,11 +16,11 @@ import { parseNdjsonStrings, filterExportedCounts, createLimitStream, -} from '../../utils/read_stream/create_stream_from_ndjson'; +} from '../../../../../utils/read_stream/create_stream_from_ndjson'; -import { ImportTimelineResponse } from './routes/utils/import_timelines'; -import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema'; -import { BadRequestError } from '../detection_engine/errors/bad_request_error'; +import { ImportTimelineResponse } from './types'; +import { ImportTimelinesSchemaRt } from '../../../schemas/timelines/import_timelines_schema'; +import { BadRequestError } from '../../../../detection_engine/errors/bad_request_error'; type ErrorFactory = (message: string) => Error; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/get_timelines_from_stream.ts similarity index 91% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/get_timelines_from_stream.ts index 16dc394ac64e9..51b858efa3fee 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_from_stream.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/get_timelines_from_stream.ts @@ -6,8 +6,8 @@ */ import uuid from 'uuid'; -import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; -import { PromiseFromStreams } from './import_timelines'; +import { createBulkErrorObject, BulkError } from '../../../../detection_engine/routes/utils'; +import { PromiseFromStreams } from './types'; export const getTupleDuplicateErrorsAndUniqueTimeline = ( timelines: PromiseFromStreams[], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts similarity index 87% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts index 4b4516e1aa1a1..21ff77e1edbdd 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts @@ -11,45 +11,30 @@ import uuid from 'uuid'; import { createPromiseFromStreams } from '@kbn/utils'; import { - TimelineStatus, - SavedTimeline, ImportTimelineResultSchema, importTimelineResultSchema, -} from '../../../../../common/types/timeline'; -import { validate } from '../../../../../common/validate'; -import { NoteResult } from '../../../../graphql/types'; -import { HapiReadableStream } from '../../../detection_engine/rules/types'; -import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; -import { createTimelines } from './create_timelines'; -import { FrameworkRequest } from '../../../framework'; -import { createTimelinesStreamFromNdJson } from '../../create_timelines_stream_from_ndjson'; - -import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; -import { CompareTimelinesStatus } from './compare_timelines_status'; -import { TimelineStatusActions } from './common'; -import { DEFAULT_ERROR } from './failure_cases'; + TimelineStatus, +} from '../../../../../../common/types/timeline'; +import { validate } from '../../../../../../common/validate'; -export type ImportedTimeline = SavedTimeline & { - savedObjectId: string | null; - version: string | null; - pinnedEventIds: string[]; - globalNotes: NoteResult[]; - eventNotes: NoteResult[]; -}; +import { createBulkErrorObject, BulkError } from '../../../../detection_engine/routes/utils'; -export type PromiseFromStreams = ImportedTimeline; +import { createTimelines } from '../create_timelines'; +import { FrameworkRequest } from '../../../../framework'; -interface ImportRegular { - timeline_id: string; - status_code: number; - message?: string; - action: TimelineStatusActions.createViaImport | TimelineStatusActions.updateViaImport; -} +import { CompareTimelinesStatus } from '../../../utils/compare_timelines_status'; +import { TimelineStatusActions } from '../../../utils/common'; +import { DEFAULT_ERROR } from '../../../utils/failure_cases'; +import { createTimelinesStreamFromNdJson } from './create_timelines_stream_from_ndjson'; +import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; +import { + ImportedTimeline, + ImportRegular, + ImportTimelineResponse, + PromiseFromStreams, +} from './types'; -export type ImportTimelineResponse = ImportRegular | BulkError; -export interface ImportTimelinesRequestParams { - body: { file: HapiReadableStream }; -} +const CHUNK_PARSED_OBJECT_SIZE = 10; export const isImportRegular = ( importTimelineResponse: ImportTimelineResponse @@ -97,8 +82,6 @@ export const setTimeline = ( }; }; -const CHUNK_PARSED_OBJECT_SIZE = 10; - export const importTimelines = async ( file: Readable, maxTimelineImportExportSize: number, @@ -106,6 +89,7 @@ export const importTimelines = async ( isImmutable?: boolean ): Promise => { const readStream = createTimelinesStreamFromNdJson(maxTimelineImportExportSize); + const parsedObjects = await createPromiseFromStreams([file, ...readStream]); const [duplicateIdErrors, uniqueParsedObjects] = getTupleDuplicateErrorsAndUniqueTimeline( diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts similarity index 88% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts index a4557036aaa63..2f51b23d73676 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { getImportTimelinesRequest } from './__mocks__/request_responses'; +import { getImportTimelinesRequest } from '../../../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; -import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; +} from '../../../../detection_engine/routes/__mocks__'; +import { TIMELINE_EXPORT_URL } from '../../../../../../common/constants'; +import { TimelineStatus, TimelineType } from '../../../../../../common/types/timeline'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; import { mockUniqueParsedObjects, @@ -29,13 +29,13 @@ import { mockCreatedTemplateTimeline, mockGetTemplateTimelineValue, mockCreatedTimeline, -} from './__mocks__/import_timelines'; +} from '../../../__mocks__/import_timelines'; import { TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE, EMPTY_TITLE_ERROR_MESSAGE, NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE, NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE, -} from './utils/failure_cases'; +} from '../../../utils/failure_cases'; describe('import timelines', () => { let server: ReturnType; @@ -74,7 +74,7 @@ describe('import timelines', () => { mockGetNote = jest.fn(); mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn(); - jest.doMock('../create_timelines_stream_from_ndjson', () => { + jest.doMock('./create_timelines_stream_from_ndjson', () => { return { createTimelinesStreamFromNdJson: jest.fn().mockReturnValue(mockParsedObjects), }; @@ -86,7 +86,7 @@ describe('import timelines', () => { }; }); - jest.doMock('./utils/get_timelines_from_stream', () => { + jest.doMock('./get_timelines_from_stream', () => { return { getTupleDuplicateErrorsAndUniqueTimeline: mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue( [mockDuplicateIdErrors, mockUniqueParsedObjects] @@ -97,23 +97,23 @@ describe('import timelines', () => { describe('Import a new timeline', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: mockCreatedTimeline, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + savePinnedEvents: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes/saved_object', () => { return { persistNote: mockPersistNote, getNote: mockGetNote @@ -150,8 +150,7 @@ describe('import timelines', () => { }; }); - const importTimelinesRoute = jest.requireActual('./import_timelines_route') - .importTimelinesRoute; + const importTimelinesRoute = jest.requireActual('./index').importTimelinesRoute; importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); @@ -225,25 +224,19 @@ describe('import timelines', () => { expect(mockPersistPinnedEventOnTimeline).toHaveBeenCalled(); }); - test('should Create a new pinned event without pinnedEventSavedObjectId', async () => { + test('should Create a new pinned event with new timeline id', async () => { const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); - expect(mockPersistPinnedEventOnTimeline.mock.calls[0][1]).toBeNull(); + expect(mockPersistPinnedEventOnTimeline.mock.calls[0][1]).toEqual( + mockCreatedTimeline.savedObjectId + ); }); test('should Create a new pinned event with pinnedEventId', async () => { const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][2]).toEqual( - mockUniqueParsedObjects[0].pinnedEventIds[0] - ); - }); - - test('should Create a new pinned event with new timelineSavedObjectId', async () => { - const mockRequest = await getImportTimelinesRequest(); - await server.inject(mockRequest, context); - expect(mockPersistPinnedEventOnTimeline.mock.calls[0][3]).toEqual( - mockCreatedTimeline.savedObjectId + mockUniqueParsedObjects[0].pinnedEventIds ); }); @@ -355,28 +348,27 @@ describe('import timelines', () => { describe('Import a timeline already exist', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline, }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + savePinnedEvents: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes/saved_object', () => { return { persistNote: mockPersistNote, }; }); - const importTimelinesRoute = jest.requireActual('./import_timelines_route') - .importTimelinesRoute; + const importTimelinesRoute = jest.requireActual('./index').importTimelinesRoute; importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); @@ -461,24 +453,24 @@ describe('import timelines', () => { describe('request validation', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( + savePinnedEvents: mockPersistPinnedEventOnTimeline.mockReturnValue( new Error('Test error') ), }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes/saved_object', () => { return { persistNote: mockPersistNote, }; @@ -490,8 +482,7 @@ describe('import timelines', () => { path: TIMELINE_EXPORT_URL, body: { id: 'someId' }, }); - const importTimelinesRoute = jest.requireActual('./import_timelines_route') - .importTimelinesRoute; + const importTimelinesRoute = jest.requireActual('./index').importTimelinesRoute; importTimelinesRoute(server.router, createMockConfig(), securitySetup); const result = server.validate(request); @@ -513,6 +504,8 @@ describe('import timeline templates', () => { let mockPersistTimeline: jest.Mock; let mockPersistPinnedEventOnTimeline: jest.Mock; let mockPersistNote: jest.Mock; + let mockGetNote: jest.Mock; + let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock; const mockNewTemplateTimelineId = 'new templateTimelineId'; beforeEach(() => { @@ -536,9 +529,10 @@ describe('import timeline templates', () => { mockPersistTimeline = jest.fn(); mockPersistPinnedEventOnTimeline = jest.fn(); mockPersistNote = jest.fn(); + mockGetNote = jest.fn(); mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn(); - jest.doMock('../create_timelines_stream_from_ndjson', () => { + jest.doMock('./create_timelines_stream_from_ndjson', () => { return { createTimelinesStreamFromNdJson: jest .fn() @@ -552,7 +546,7 @@ describe('import timeline templates', () => { }; }); - jest.doMock('./utils/get_timelines_from_stream', () => { + jest.doMock('./get_timelines_from_stream', () => { return { getTupleDuplicateErrorsAndUniqueTimeline: mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue( [mockDuplicateIdErrors, mockUniqueParsedTemplateTimelineObjects] @@ -567,30 +561,30 @@ describe('import timeline templates', () => { describe('Import a new timeline template', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: mockCreatedTemplateTimeline, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + savePinnedEvents: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes/saved_object', () => { return { persistNote: mockPersistNote, + getNote: mockGetNote.mockResolvedValueOnce(mockUniqueParsedObjects[0].globalNotes[0]), }; }); - const importTimelinesRoute = jest.requireActual('./import_timelines_route') - .importTimelinesRoute; + const importTimelinesRoute = jest.requireActual('./index').importTimelinesRoute; importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); @@ -717,32 +711,31 @@ describe('import timeline templates', () => { describe('Import a timeline template already exist', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [mockGetTemplateTimelineValue], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue( + mockGetTemplateTimelineValue + ), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: mockCreatedTemplateTimeline, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + savePinnedEvents: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes/saved_object', () => { return { persistNote: mockPersistNote, }; }); - const importTimelinesRoute = jest.requireActual('./import_timelines_route') - .importTimelinesRoute; + const importTimelinesRoute = jest.requireActual('./index').importTimelinesRoute; importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); @@ -889,24 +882,24 @@ describe('import timeline templates', () => { describe('request validation', () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( + savePinnedEvents: mockPersistPinnedEventOnTimeline.mockReturnValue( new Error('Test error') ), }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes/saved_object', () => { return { persistNote: mockPersistNote, }; @@ -918,8 +911,7 @@ describe('import timeline templates', () => { path: TIMELINE_EXPORT_URL, body: { id: 'someId' }, }); - const importTimelinesRoute = jest.requireActual('./import_timelines_route') - .importTimelinesRoute; + const importTimelinesRoute = jest.requireActual('./index').importTimelinesRoute; importTimelinesRoute(server.router, createMockConfig(), securitySetup); const result = server.validate(request); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts similarity index 74% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts index 585231190292b..603aad16dd9c6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts @@ -8,18 +8,20 @@ import { extname } from 'path'; import { Readable } from 'stream'; -import type { SecuritySolutionPluginRouter } from '../../../types'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; +import { TIMELINE_IMPORT_URL } from '../../../../../../common/constants'; -import { SetupPlugins } from '../../../plugin'; -import { ConfigType } from '../../../config'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; -import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; +import { SetupPlugins } from '../../../../../plugin'; +import { ConfigType } from '../../../../../config'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; +import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; -import { importTimelines } from './utils/import_timelines'; -import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; -import { buildFrameworkRequest } from './utils/common'; +import { importTimelines } from './helpers'; +import { ImportTimelinesPayloadSchemaRt } from '../../../schemas/timelines/import_timelines_schema'; +import { buildFrameworkRequest } from '../../../utils/common'; + +export { importTimelines } from './helpers'; export const importTimelinesRoute = ( router: SecuritySolutionPluginRouter, @@ -58,7 +60,6 @@ export const importTimelinesRoute = ( body: `Invalid file extension ${fileExtension}`, }); } - const frameworkRequest = await buildFrameworkRequest(context, security, request); const res = await importTimelines( diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/types.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/types.ts new file mode 100644 index 0000000000000..cc4221cba1098 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { BulkError } from '../../../../detection_engine/routes/utils'; + +import { SavedTimeline } from '../../../../../../common/types/timeline'; +import { NoteResult } from '../../../../../graphql/types'; +import { HapiReadableStream } from '../../../../detection_engine/rules/types'; +import { TimelineStatusActions } from '../../../utils/common'; + +export type ImportedTimeline = SavedTimeline & { + savedObjectId: string | null; + version: string | null; + pinnedEventIds: string[]; + globalNotes: NoteResult[]; + eventNotes: NoteResult[]; +}; + +export type PromiseFromStreams = ImportedTimeline; + +export interface ImportRegular { + timeline_id: string; + status_code: number; + message?: string; + action: TimelineStatusActions.createViaImport | TimelineStatusActions.updateViaImport; +} + +export type ImportTimelineResponse = ImportRegular | BulkError; +export interface ImportTimelinesRequestParams { + body: { file: HapiReadableStream }; +} diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts similarity index 80% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts index 6a7159bcfb322..8f583dbcc05a8 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.test.ts @@ -5,29 +5,29 @@ * 2.0. */ -import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../../../../security/server'; import { serverMock, requestContextMock, createMockConfig, -} from '../../detection_engine/routes/__mocks__'; +} from '../../../../detection_engine/routes/__mocks__'; import { getUpdateTimelinesRequest, inputTimeline, updateTimelineWithTimelineId, updateTemplateTimelineWithTimelineId, -} from './__mocks__/request_responses'; +} from '../../../__mocks__/request_responses'; import { mockGetCurrentUser, mockGetTimelineValue, mockGetTemplateTimelineValue, -} from './__mocks__/import_timelines'; +} from '../../../__mocks__/import_timelines'; import { UPDATE_TIMELINE_ERROR_MESSAGE, UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, -} from './utils/failure_cases'; +} from '../../../utils/failure_cases'; describe('update timelines', () => { let server: ReturnType; @@ -65,29 +65,28 @@ describe('update timelines', () => { describe('Manipulate timeline', () => { describe('Update an existing timeline', () => { beforeEach(async () => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTimelineValue), persistTimeline: mockPersistTimeline.mockReturnValue({ timeline: updateTimelineWithTimelineId.timeline, }), }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const updateTimelinesRoute = jest.requireActual('./update_timelines_route') - .updateTimelinesRoute; + const updateTimelinesRoute = jest.requireActual('./index').updateTimelinesRoute; updateTimelinesRoute(server.router, createMockConfig(), securitySetup); const mockRequest = getUpdateTimelinesRequest(updateTimelineWithTimelineId); @@ -131,28 +130,27 @@ describe('update timelines', () => { describe("Update a timeline that doesn't exist", () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline, }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const updateTimelinesRoute = jest.requireActual('./update_timelines_route') - .updateTimelinesRoute; + const updateTimelinesRoute = jest.requireActual('./index').updateTimelinesRoute; updateTimelinesRoute(server.router, createMockConfig(), securitySetup); }); @@ -172,10 +170,10 @@ describe('update timelines', () => { describe('Manipulate timeline template', () => { describe('Update an existing timeline template', () => { beforeEach(async () => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue({ timeline: [mockGetTemplateTimelineValue], }), persistTimeline: mockPersistTimeline.mockReturnValue({ @@ -184,20 +182,19 @@ describe('update timelines', () => { }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const updateTimelinesRoute = jest.requireActual('./update_timelines_route') - .updateTimelinesRoute; + const updateTimelinesRoute = jest.requireActual('./index').updateTimelinesRoute; updateTimelinesRoute(server.router, createMockConfig(), securitySetup); const mockRequest = getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId); @@ -253,30 +250,27 @@ describe('update timelines', () => { describe("Update a timeline template that doesn't exist", () => { beforeEach(() => { - jest.doMock('../saved_object', () => { + jest.doMock('../../../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), persistTimeline: mockPersistTimeline, }; }); - jest.doMock('../../pinned_event/saved_object', () => { + jest.doMock('../../../saved_object/pinned_events', () => { return { persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); - jest.doMock('../../note/saved_object', () => { + jest.doMock('../../../saved_object/notes', () => { return { persistNote: mockPersistNote, }; }); - const updateTimelinesRoute = jest.requireActual('./update_timelines_route') - .updateTimelinesRoute; + const updateTimelinesRoute = jest.requireActual('./index').updateTimelinesRoute; updateTimelinesRoute(server.router, createMockConfig(), securitySetup); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts similarity index 74% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts rename to x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts index 9b8a689a092a2..61880789eca84 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts @@ -5,20 +5,20 @@ * 2.0. */ -import type { SecuritySolutionPluginRouter } from '../../../types'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { TIMELINE_URL } from '../../../../common/constants'; +import { TIMELINE_URL } from '../../../../../../common/constants'; -import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; -import { ConfigType } from '../../..'; +import { SetupPlugins } from '../../../../../plugin'; +import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; +import { ConfigType } from '../../../../..'; -import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; +import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; -import { updateTimelineSchema } from './schemas/update_timelines_schema'; -import { buildFrameworkRequest, TimelineStatusActions } from './utils/common'; -import { createTimelines } from './utils/create_timelines'; -import { CompareTimelinesStatus } from './utils/compare_timelines_status'; +import { patchTimelineSchema } from '../../../schemas/timelines/patch_timelines_schema'; +import { buildFrameworkRequest, TimelineStatusActions } from '../../../utils/common'; +import { createTimelines } from '../create_timelines'; +import { CompareTimelinesStatus } from '../../../utils/compare_timelines_status'; export const updateTimelinesRoute = ( router: SecuritySolutionPluginRouter, @@ -29,7 +29,7 @@ export const updateTimelinesRoute = ( { path: TIMELINE_URL, validate: { - body: buildRouteValidationWithExcess(updateTimelineSchema), + body: buildRouteValidationWithExcess(patchTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts deleted file mode 100644 index fb88bcd4d7960..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ /dev/null @@ -1,206 +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 { isEmpty } from 'lodash/fp'; - -import moment from 'moment'; -import * as timelineLib from '../../saved_object'; -import * as pinnedEventLib from '../../../pinned_event/saved_object'; -import * as noteLib from '../../../note/saved_object'; -import { FrameworkRequest } from '../../../framework'; -import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/timeline'; -import { SavedNote } from '../../../../../common/types/timeline/note'; -import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; - -export const saveTimelines = ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null, - isImmutable?: boolean -): Promise => { - return timelineLib.persistTimeline( - frameworkRequest, - timelineSavedObjectId ?? null, - timelineVersion ?? null, - timeline, - isImmutable - ); -}; - -export const savePinnedEvents = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - pinnedEventIds: string[] -) => - Promise.all( - pinnedEventIds.map((eventId) => - pinnedEventLib.persistPinnedEventOnTimeline( - frameworkRequest, - null, // pinnedEventSavedObjectId - eventId, - timelineSavedObjectId - ) - ) - ); - -const getNewNote = async ( - frameworkRequest: FrameworkRequest, - note: NoteResult, - timelineSavedObjectId: string, - overrideOwner: boolean -): Promise => { - let savedNote = note; - try { - savedNote = await noteLib.getNote(frameworkRequest, note.noteId); - // eslint-disable-next-line no-empty - } catch (e) {} - return overrideOwner - ? { - eventId: note.eventId, - note: note.note, - timelineId: timelineSavedObjectId, - } - : { - eventId: savedNote.eventId, - note: savedNote.note, - created: savedNote.created, - createdBy: savedNote.createdBy, - updated: savedNote.updated, - updatedBy: savedNote.updatedBy, - timelineId: timelineSavedObjectId, - }; -}; - -export const saveNotes = async ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[], - overrideOwner: boolean = true -) => { - return Promise.all( - newNotes?.map(async (note) => { - const newNote = await getNewNote( - frameworkRequest, - note, - timelineSavedObjectId, - overrideOwner - ); - return noteLib.persistNote( - frameworkRequest, - overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null, - timelineVersion ?? null, - newNote, - overrideOwner - ); - }) ?? [] - ); -}; - -interface CreateTimelineProps { - frameworkRequest: FrameworkRequest; - timeline: SavedTimeline; - timelineSavedObjectId?: string | null; - timelineVersion?: string | null; - overrideNotesOwner?: boolean; - pinnedEventIds?: string[] | null; - notes?: NoteResult[]; - existingNoteIds?: string[]; - isImmutable?: boolean; -} - -/** allow overrideNotesOwner means overriding by current username, - * disallow overrideNotesOwner means keep the original username. - * overrideNotesOwner = false only happens when import timeline templates, - * as we want to keep the original creator for notes - **/ -export const createTimelines = async ({ - frameworkRequest, - timeline, - timelineSavedObjectId = null, - timelineVersion = null, - pinnedEventIds = null, - notes = [], - existingNoteIds = [], - isImmutable, - overrideNotesOwner = true, -}: CreateTimelineProps): Promise => { - const timerangeStart = isImmutable - ? moment().subtract(24, 'hours').toISOString() - : timeline.dateRange?.start; - const timerangeEnd = isImmutable ? moment().toISOString() : timeline.dateRange?.end; - const responseTimeline = await saveTimelines( - frameworkRequest, - { ...timeline, dateRange: { start: timerangeStart, end: timerangeEnd } }, - timelineSavedObjectId, - timelineVersion, - isImmutable - ); - const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; - const newTimelineVersion = responseTimeline.timeline.version; - - let myPromises: unknown[] = []; - if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) { - myPromises = [ - ...myPromises, - savePinnedEvents( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - pinnedEventIds - ), - ]; - } - if (!isEmpty(notes)) { - myPromises = [ - ...myPromises, - saveNotes( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - newTimelineVersion, - existingNoteIds, - notes, - overrideNotesOwner - ), - ]; - } - - if (myPromises.length > 0) { - await Promise.all(myPromises); - } - - return responseTimeline; -}; - -export const getTimeline = async ( - frameworkRequest: FrameworkRequest, - savedObjectId: string -): Promise => { - let timeline = null; - try { - timeline = await timelineLib.getTimeline(frameworkRequest, savedObjectId); - // eslint-disable-next-line no-empty - } catch (e) {} - return timeline; -}; - -export const getTemplateTimeline = async ( - frameworkRequest: FrameworkRequest, - templateTimelineId: string -): Promise => { - let templateTimeline = null; - try { - templateTimeline = await timelineLib.getTimelineByTemplateTimelineId( - frameworkRequest, - templateTimelineId - ); - } catch (e) { - return null; - } - return templateTimeline?.timeline[0] ?? null; -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.ts deleted file mode 100644 index 8ba2174179b16..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_install.ts +++ /dev/null @@ -1,21 +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 { ImportTimelinesSchema } from '../schemas/import_timelines_schema'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; - -export const getTimelinesToInstall = ( - timelinesFromFileSystem: ImportTimelinesSchema[], - installedTimelines: TimelineSavedObject[] -): ImportTimelinesSchema[] => { - return timelinesFromFileSystem.filter( - (timeline) => - !installedTimelines.some( - (installedTimeline) => installedTimeline.templateTimelineId === timeline.templateTimelineId - ) - ); -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts deleted file mode 100644 index 6bdc61c1fa595..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/get_timelines_to_update.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ImportTimelinesSchema } from '../schemas/import_timelines_schema'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; - -export const getTimelinesToUpdate = ( - timelinesFromFileSystem: ImportTimelinesSchema[], - installedTimelines: TimelineSavedObject[] -): ImportTimelinesSchema[] => { - return timelinesFromFileSystem.filter((timeline) => - installedTimelines.some((installedTimeline) => { - return ( - timeline.templateTimelineId === installedTimeline.templateTimelineId && - timeline.templateTimelineVersion! > installedTimeline.templateTimelineVersion! - ); - }) - ); -}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/index.ts new file mode 100644 index 0000000000000..2b5c3b19ee74a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * as timeline from './timelines'; +export * as note from './notes'; +export * as pinnedEvent from './pinned_events'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/get_overridable_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/get_overridable_note.ts new file mode 100644 index 0000000000000..1ff1c37a16357 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/get_overridable_note.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedNote } from '../../../../../common/types/timeline/note'; +import { NoteResult } from '../../../../graphql/types'; +import { FrameworkRequest } from '../../../framework'; +import { getNote } from './saved_object'; + +export const getOverridableNote = async ( + frameworkRequest: FrameworkRequest, + note: NoteResult, + timelineSavedObjectId: string, + overrideOwner: boolean +): Promise => { + let savedNote = note; + try { + savedNote = await getNote(frameworkRequest, note.noteId); + // eslint-disable-next-line no-empty + } catch (e) {} + return overrideOwner + ? { + eventId: note.eventId, + note: note.note, + timelineId: timelineSavedObjectId, + } + : { + eventId: savedNote.eventId, + note: savedNote.note, + created: savedNote.created, + createdBy: savedNote.createdBy, + updated: savedNote.updated, + updatedBy: savedNote.updatedBy, + timelineId: timelineSavedObjectId, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts new file mode 100644 index 0000000000000..9addf0f80e124 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FrameworkRequest } from '../../../framework'; +import { PageInfoNote, ResponseNote, ResponseNotes, SortNote } from '../../../../graphql/types'; +import { SavedNote, NoteSavedObject } from '../../../../../common/types/timeline/note'; + +export * from './saved_object'; +export interface Notes { + deleteNote: (request: FrameworkRequest, noteIds: string[]) => Promise; + deleteNoteByTimelineId: (request: FrameworkRequest, noteIds: string) => Promise; + getNote: (request: FrameworkRequest, noteId: string) => Promise; + getNotesByEventId: (request: FrameworkRequest, noteId: string) => Promise; + getNotesByTimelineId: (request: FrameworkRequest, noteId: string) => Promise; + getAllNotes: ( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null + ) => Promise; + persistNote: ( + request: FrameworkRequest, + noteId: string | null, + version: string | null, + note: SavedNote, + overrideOwner: boolean + ) => Promise; + convertSavedObjectToSavedNote: ( + savedObject: unknown, + timelineVersion?: string | undefined | null + ) => NoteSavedObject; +} diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.ts new file mode 100644 index 0000000000000..7f6a355f43df0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FrameworkRequest } from '../../../framework'; +import { NoteResult } from '../../../../graphql/types'; +import { persistNote } from './saved_object'; +import { getOverridableNote } from './get_overridable_note'; + +export const persistNotes = async ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[], + overrideOwner: boolean = true +) => { + return Promise.all( + newNotes?.map(async (note) => { + const newNote = await getOverridableNote( + frameworkRequest, + note, + timelineSavedObjectId, + overrideOwner + ); + return persistNote( + frameworkRequest, + overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null, + timelineVersion ?? null, + newNote, + overrideOwner + ); + }) ?? [] + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts similarity index 81% rename from x-pack/plugins/security_solution/server/lib/note/saved_object.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts index 79c50aba7141c..8016fdf12881d 100644 --- a/x-pack/plugins/security_solution/server/lib/note/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts @@ -13,51 +13,25 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; -import { AuthenticatedUser } from '../../../../security/common/model'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { SavedObjectsFindOptions } from '../../../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../../../security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../../../common/constants'; import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject, -} from '../../../common/types/timeline/note'; +} from '../../../../../common/types/timeline/note'; import { PageInfoNote, ResponseNote, ResponseNotes, SortNote, NoteResult, -} from '../../graphql/types'; -import { FrameworkRequest } from '../framework'; -import { noteSavedObjectType } from './saved_object_mappings'; -import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; -import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; -import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; - -export interface Note { - deleteNote: (request: FrameworkRequest, noteIds: string[]) => Promise; - deleteNoteByTimelineId: (request: FrameworkRequest, noteIds: string) => Promise; - getNote: (request: FrameworkRequest, noteId: string) => Promise; - getNotesByEventId: (request: FrameworkRequest, noteId: string) => Promise; - getNotesByTimelineId: (request: FrameworkRequest, noteId: string) => Promise; - getAllNotes: ( - request: FrameworkRequest, - pageInfo: PageInfoNote | null, - search: string | null, - sort: SortNote | null - ) => Promise; - persistNote: ( - request: FrameworkRequest, - noteId: string | null, - version: string | null, - note: SavedNote, - overrideOwner: boolean - ) => Promise; - convertSavedObjectToSavedNote: ( - savedObject: unknown, - timelineVersion?: string | undefined | null - ) => NoteSavedObject; -} +} from '../../../../graphql/types'; +import { FrameworkRequest } from '../../../framework'; +import { noteSavedObjectType } from '../../saved_object_mappings/notes'; +import { convertSavedObjectToSavedTimeline, pickSavedTimeline } from '../timelines'; +import { timelineSavedObjectType } from '../../saved_object_mappings'; export const deleteNote = async (request: FrameworkRequest, noteIds: string[]) => { const savedObjectsClient = request.context.core.savedObjects.client; diff --git a/x-pack/plugins/security_solution/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts similarity index 88% rename from x-pack/plugins/security_solution/server/lib/pinned_event/saved_object.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts index d59f31504278b..6467d1d43d807 100644 --- a/x-pack/plugins/security_solution/server/lib/pinned_event/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/pinned_events/index.ts @@ -11,21 +11,25 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; -import { AuthenticatedUser } from '../../../../security/common/model'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { SavedObjectsFindOptions } from '../../../../../../../../src/core/server'; +import { AuthenticatedUser } from '../../../../../../security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../../../common/constants'; import { PinnedEventSavedObject, PinnedEventSavedObjectRuntimeType, SavedPinnedEvent, -} from '../../../common/types/timeline/pinned_event'; -import { FrameworkRequest } from '../framework'; +} from '../../../../../common/types/timeline/pinned_event'; +import { FrameworkRequest } from '../../../framework'; -import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; -import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; -import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; -import { pinnedEventSavedObjectType } from './saved_object_mappings'; -import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; +import { + PageInfoNote, + SortNote, + PinnedEvent as PinnedEventResponse, +} from '../../../../graphql/types'; +import { pickSavedTimeline } from '../../saved_object/timelines'; +import { convertSavedObjectToSavedTimeline } from '../timelines'; +import { pinnedEventSavedObjectType } from '../../saved_object_mappings/pinned_events'; +import { timelineSavedObjectType } from '../../saved_object_mappings'; export interface PinnedEvent { deletePinnedEventOnTimeline: ( @@ -233,6 +237,22 @@ const getAllSavedPinnedEvents = async ( ); }; +export const savePinnedEvents = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + pinnedEventIds: string[] +) => + Promise.all( + pinnedEventIds.map((eventId) => + persistPinnedEventOnTimeline( + frameworkRequest, + null, // pinnedEventSavedObjectId + eventId, + timelineSavedObjectId + ) + ) + ); + export const convertSavedObjectToSavedPinnedEvent = ( savedObject: unknown, timelineVersion?: string | undefined | null diff --git a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/convert_saved_object_to_savedtimeline.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/convert_saved_object_to_savedtimeline.ts index b28846ca3c03c..27826a92b6d96 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/convert_saved_object_to_savedtimeline.ts @@ -16,7 +16,7 @@ import { TimelineSavedObject, TimelineType, TimelineStatus, -} from '../../../common/types/timeline'; +} from '../../../../../common/types/timeline'; // TODO: Added to support legacy TimelineType.draft, can be removed in 7.10 export const TimelineSavedObjectWithDraftRuntimeType = intersection([ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts similarity index 95% rename from x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts index d329fa31a3f04..61e25d419a0e4 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.test.ts @@ -5,28 +5,28 @@ * 2.0. */ -import { FrameworkRequest } from '../framework'; -import { mockGetTimelineValue, mockSavedObject } from './routes/__mocks__/import_timelines'; +import { FrameworkRequest } from '../../../framework'; +import { mockGetTimelineValue, mockSavedObject } from '../../__mocks__/import_timelines'; import { convertStringToBase64, getExistingPrepackagedTimelines, getAllTimeline, AllTimelinesResponse, -} from './saved_object'; +} from '.'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; -import { getNotesByTimelineId } from '../note/saved_object'; -import { getAllPinnedEventsByTimelineId } from '../pinned_event/saved_object'; +import { getNotesByTimelineId } from '../notes/saved_object'; +import { getAllPinnedEventsByTimelineId } from '../pinned_events'; jest.mock('./convert_saved_object_to_savedtimeline', () => ({ convertSavedObjectToSavedTimeline: jest.fn(), })); -jest.mock('../note/saved_object', () => ({ +jest.mock('../notes/saved_object', () => ({ getNotesByTimelineId: jest.fn().mockResolvedValue([]), })); -jest.mock('../pinned_event/saved_object', () => ({ +jest.mock('../pinned_events', () => ({ getAllPinnedEventsByTimelineId: jest.fn().mockResolvedValue([]), })); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts similarity index 92% rename from x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts index 6d09ef683dd49..0f624ef5420bc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts @@ -7,17 +7,17 @@ import { getOr } from 'lodash/fp'; -import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { NoteSavedObject } from '../../../common/types/timeline/note'; -import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event'; +import { SavedObjectsFindOptions } from '../../../../../../../../src/core/server'; +import { UNAUTHENTICATED_USER } from '../../../../../common/constants'; +import { NoteSavedObject } from '../../../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event'; import { SavedTimeline, TimelineSavedObject, TimelineTypeLiteralWithNull, ExportTimelineNotFoundError, TimelineStatusLiteralWithNull, -} from '../../../common/types/timeline'; +} from '../../../../../common/types/timeline'; import { ResponseTimeline, PageInfoTimeline, @@ -27,15 +27,17 @@ import { TimelineType, TimelineStatus, Maybe, -} from '../../graphql/types'; -import { FrameworkRequest } from '../framework'; -import * as note from '../note/saved_object'; -import * as pinnedEvent from '../pinned_event/saved_object'; +} from '../../../../graphql/types'; +import { FrameworkRequest } from '../../../framework'; +import * as note from '../notes/saved_object'; +import * as pinnedEvent from '../pinned_events'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { pickSavedTimeline } from './pick_saved_timeline'; -import { timelineSavedObjectType } from './saved_object_mappings'; -import { draftTimelineDefaults } from './default_timeline'; -import { AuthenticatedUser } from '../../../../security/server'; +import { timelineSavedObjectType } from '../../saved_object_mappings/'; +import { draftTimelineDefaults } from '../../utils/default_timeline'; +import { AuthenticatedUser } from '../../../../../../security/server'; +export { pickSavedTimeline } from './pick_saved_timeline'; +export { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; interface ResponseTimelines { timeline: TimelineSavedObject[]; @@ -126,6 +128,18 @@ export const getTimeline = async ( return getSavedTimeline(request, timelineIdToUse); }; +export const getTimelineOrNull = async ( + frameworkRequest: FrameworkRequest, + savedObjectId: string +): Promise => { + let timeline = null; + try { + timeline = await getTimeline(frameworkRequest, savedObjectId); + // eslint-disable-next-line no-empty + } catch (e) {} + return timeline; +}; + export const getTimelineByTemplateTimelineId = async ( request: FrameworkRequest, templateTimelineId: string @@ -140,6 +154,19 @@ export const getTimelineByTemplateTimelineId = async ( return getAllSavedTimeline(request, options); }; +export const getTimelineTemplateOrNull = async ( + frameworkRequest: FrameworkRequest, + templateTimelineId: string +): Promise => { + let templateTimeline = null; + try { + templateTimeline = await getTimelineByTemplateTimelineId(frameworkRequest, templateTimelineId); + } catch (e) { + return null; + } + return templateTimeline?.timeline[0] ?? null; +}; + /** The filter here is able to handle the legacy data, * which has no timelineType exists in the savedObject */ const getTimelineTypeFilter = ( diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts index 26f6d0eae4f93..94e70e4eb001b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { AuthenticatedUser } from '../../../../security/common/model'; +import { AuthenticatedUser } from '../../../../../../security/common/model'; -import { TimelineStatus, TimelineType } from '../../../common/types/timeline'; +import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { pickSavedTimeline } from './pick_saved_timeline'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts similarity index 90% rename from x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts index 5b6752c57ecb1..a28084cd78154 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts @@ -6,9 +6,9 @@ */ import { isEmpty } from 'lodash/fp'; -import { AuthenticatedUser } from '../../../../security/common/model'; -import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { SavedTimeline, TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { AuthenticatedUser } from '../../../../../../security/common/model'; +import { UNAUTHENTICATED_USER } from '../../../../../common/constants'; +import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; export const pickSavedTimeline = ( timelineId: string | null, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/index.ts new file mode 100644 index 0000000000000..2d3e39685090f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './notes'; +export * from './pinned_events'; +export * from './timelines'; diff --git a/x-pack/plugins/security_solution/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts similarity index 87% rename from x-pack/plugins/security_solution/server/lib/note/saved_object_mappings.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts index 02cb4ee2457cb..5815747d3e720 100644 --- a/x-pack/plugins/security_solution/server/lib/note/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsType } from '../../../../../../src/core/server'; +import { SavedObjectsType } from '../../../../../../../src/core/server'; export const noteSavedObjectType = 'siem-ui-timeline-note'; @@ -35,7 +35,7 @@ export const noteSavedObjectMappings: SavedObjectsType['mappings'] = { }, }; -export const type: SavedObjectsType = { +export const noteType: SavedObjectsType = { name: noteSavedObjectType, hidden: false, namespaceType: 'single', diff --git a/x-pack/plugins/security_solution/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts similarity index 86% rename from x-pack/plugins/security_solution/server/lib/pinned_event/saved_object_mappings.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts index f8163c20ec803..fbbffe35a58c0 100644 --- a/x-pack/plugins/security_solution/server/lib/pinned_event/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/pinned_events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsType } from '../../../../../../src/core/server'; +import { SavedObjectsType } from '../../../../../../../src/core/server'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; @@ -32,7 +32,7 @@ export const pinnedEventSavedObjectMappings: SavedObjectsType['mappings'] = { }, }; -export const type: SavedObjectsType = { +export const pinnedEventType: SavedObjectsType = { name: pinnedEventSavedObjectType, hidden: false, namespaceType: 'single', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts rename to x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts index 8e0d88a9b9a6f..a2de616e68c79 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/timelines.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsType } from '../../../../../../src/core/server'; +import { SavedObjectsType } from '../../../../../../../src/core/server'; export const timelineSavedObjectType = 'siem-ui-timeline'; @@ -317,7 +317,7 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { }, }; -export const type: SavedObjectsType = { +export const timelineType: SavedObjectsType = { name: timelineSavedObjectType, hidden: false, namespaceType: 'single', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/clean_draft_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/clean_draft_timelines_schema.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/clean_draft_timelines_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/clean_draft_timelines_schema.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_draft_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/get_draft_timelines_schema.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_draft_timelines_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/get_draft_timelines_schema.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/index.ts new file mode 100644 index 0000000000000..d98958dee4510 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/schemas/draft_timelines/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 { cleanDraftTimelineSchema } from './clean_draft_timelines_schema'; +export { getDraftTimelineSchema } from './get_draft_timelines_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/notes/index.ts similarity index 87% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/schemas.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/notes/index.ts index c5951787adcd5..de1e357896353 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/schemas/notes/index.ts @@ -11,4 +11,3 @@ import { SavedNoteRuntimeType } from '../../../../../common/types/timeline/note' export const eventNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); export const globalNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); -export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string)); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/schemas/pinned_events/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/pinned_events/index.ts new file mode 100644 index 0000000000000..29afda10dce80 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/schemas/pinned_events/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as runtimeTypes from 'io-ts'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string)); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/create_timelines_schema.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/create_timelines_schema.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/export_timelines_schema.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/export_timelines_schema.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/get_timeline_by_id_schema.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/get_timeline_by_id_schema.ts diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/import_timelines_schema.ts similarity index 93% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/import_timelines_schema.ts index 18a9fba780549..c1742a3ac1d85 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/import_timelines_schema.ts @@ -8,10 +8,11 @@ import * as rt from 'io-ts'; import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; - -import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; import { unionWithNullType } from '../../../../../common/utility_types'; +import { eventNotes, globalNotes } from '../notes'; +import { pinnedEventIds } from '../pinned_events'; + export const ImportTimelinesSchemaRt = rt.intersection([ SavedTimelineRuntimeType, rt.type({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/index.ts new file mode 100644 index 0000000000000..e85ae2ab4ae86 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export * from './create_timelines_schema'; +export * from './export_timelines_schema'; +export * from './get_timeline_by_id_schema'; +export * from './patch_timelines_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/update_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/patch_timelines_schema.ts similarity index 92% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/update_timelines_schema.ts rename to x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/patch_timelines_schema.ts index 7d3679e3ad910..bb45be082f5f9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/update_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/schemas/timelines/patch_timelines_schema.ts @@ -10,7 +10,7 @@ import * as rt from 'io-ts'; import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; import { unionWithNullType } from '../../../../../common/utility_types'; -export const updateTimelineSchema = rt.type({ +export const patchTimelineSchema = rt.type({ timeline: SavedTimelineRuntimeType, timelineId: unionWithNullType(rt.string), version: unionWithNullType(rt.string), diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/check_timelines_status.ts similarity index 51% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/check_timelines_status.ts index f03201d07a45c..560df1112ac58 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/check_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/check_timelines_status.ts @@ -6,18 +6,57 @@ */ import path, { join, resolve } from 'path'; +import * as rt from 'io-ts'; +import { + TimelineSavedToReturnObjectRuntimeType, + TimelineSavedObject, +} from '../../../../common/types/timeline'; -import { TimelineSavedObject } from '../../../../../common/types/timeline'; +import { + ImportTimelinesSchema, + ImportTimelinesSchemaRt, +} from '../schemas/timelines/import_timelines_schema'; +import { unionWithNullType } from '../../../../common/utility_types'; -import { FrameworkRequest } from '../../../framework'; +import { FrameworkRequest } from '../../framework'; -import { getExistingPrepackagedTimelines } from '../../saved_object'; - -import { CheckTimelineStatusRt } from '../schemas/check_timelines_status_schema'; +import { getExistingPrepackagedTimelines } from '../saved_object/timelines'; import { loadData, getReadables } from './common'; -import { getTimelinesToInstall } from './get_timelines_to_install'; -import { getTimelinesToUpdate } from './get_timelines_to_update'; + +export const checkTimelineStatusRt = rt.type({ + timelinesToInstall: rt.array(unionWithNullType(ImportTimelinesSchemaRt)), + timelinesToUpdate: rt.array(unionWithNullType(ImportTimelinesSchemaRt)), + prepackagedTimelines: rt.array(unionWithNullType(TimelineSavedToReturnObjectRuntimeType)), +}); + +export type CheckTimelineStatusRt = rt.TypeOf; + +export const getTimelinesToUpdate = ( + timelinesFromFileSystem: ImportTimelinesSchema[], + installedTimelines: TimelineSavedObject[] +): ImportTimelinesSchema[] => { + return timelinesFromFileSystem.filter((timeline) => + installedTimelines.some((installedTimeline) => { + return ( + timeline.templateTimelineId === installedTimeline.templateTimelineId && + timeline.templateTimelineVersion! > installedTimeline.templateTimelineVersion! + ); + }) + ); +}; + +export const getTimelinesToInstall = ( + timelinesFromFileSystem: ImportTimelinesSchema[], + installedTimelines: TimelineSavedObject[] +): ImportTimelinesSchema[] => { + return timelinesFromFileSystem.filter( + (timeline) => + !installedTimelines.some( + (installedTimeline) => installedTimeline.templateTimelineId === timeline.templateTimelineId + ) + ); +}; export const checkTimelinesStatus = async ( frameworkRequest: FrameworkRequest, @@ -30,7 +69,7 @@ export const checkTimelinesStatus = async ( timeline: TimelineSavedObject[]; }; const dir = resolve( - join(__dirname, filePath ?? '../../../detection_engine/rules/prepackaged_timelines') + join(__dirname, filePath ?? '../../detection_engine/rules/prepackaged_timelines') ); const file = fileName ?? 'index.ndjson'; const dataPath = path.join(dir, file); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts similarity index 97% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index 6cad35ea2e2d7..443742ae88f0d 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -12,10 +12,10 @@ import { Readable } from 'stream'; import { createListStream } from '@kbn/utils'; import { KibanaRequest } from 'src/core/server'; -import { SetupPlugins } from '../../../../plugin'; -import type { SecuritySolutionRequestHandlerContext } from '../../../../types'; +import { SetupPlugins } from '../../../plugin'; +import type { SecuritySolutionRequestHandlerContext } from '../../../types'; -import { FrameworkRequest } from '../../../framework'; +import { FrameworkRequest } from '../../framework'; export const buildFrameworkRequest = async ( context: SecuritySolutionRequestHandlerContext, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/compare_timelines_status.test.ts similarity index 90% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/compare_timelines_status.test.ts index 2117fd60cdbe1..7fb49a3923a25 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/compare_timelines_status.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { TimelineType, TimelineStatus } from '../../../../../common/types/timeline'; -import { FrameworkRequest } from '../../../framework'; +import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { FrameworkRequest } from '../../framework'; import { mockUniqueParsedObjects, @@ -46,12 +46,10 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => { + jest.doMock('../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), }; }); @@ -122,12 +120,10 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => { + jest.doMock('../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), }; }); @@ -189,11 +185,11 @@ describe('CompareTimelinesStatus', () => { let timelineObj: TimelinesStatusType; beforeEach(async () => { - jest.doMock('../../saved_object', () => ({ - getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [mockGetTemplateTimelineValue], - }), + jest.doMock('../saved_object/timelines', () => ({ + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue( + mockGetTemplateTimelineValue + ), })); const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status') @@ -275,9 +271,9 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => ({ - getTimeline: mockGetTimeline, - getTimelineByTemplateTimelineId: mockGetTemplateTimeline, + jest.doMock('../saved_object/timelines', () => ({ + getTimelineOrNull: mockGetTimeline, + getTimelineTemplateOrNull: mockGetTemplateTimeline, })); const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status') @@ -366,12 +362,10 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => { + jest.doMock('../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), }; }); @@ -447,12 +441,10 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => { + jest.doMock('../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue(null), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [], - }), + getTimelineOrNull: mockGetTimeline.mockReturnValue(null), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), }; }); @@ -535,15 +527,13 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => { + jest.doMock('../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue({ + getTimelineOrNull: mockGetTimeline.mockReturnValue({ ...mockGetTimelineValue, status: TimelineStatus.immutable, }), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [], - }), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue(null), }; }); @@ -609,14 +599,15 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => { + jest.doMock('../saved_object/timelines', () => { return { - getTimeline: mockGetTimeline.mockReturnValue({ + getTimelineOrNull: mockGetTimeline.mockReturnValue({ ...mockGetTemplateTimelineValue, status: TimelineStatus.immutable, }), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [{ ...mockGetTemplateTimelineValue, status: TimelineStatus.immutable }], + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue({ + ...mockGetTemplateTimelineValue, + status: TimelineStatus.immutable, }), }; }); @@ -683,9 +674,9 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => ({ - getTimeline: mockGetTimeline, - getTimelineByTemplateTimelineId: mockGetTemplateTimeline, + jest.doMock('../saved_object/timelines', () => ({ + getTimelineOrNull: mockGetTimeline, + getTimelineTemplateOrNull: mockGetTemplateTimeline, })); const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status') @@ -745,11 +736,11 @@ describe('CompareTimelinesStatus', () => { }); beforeEach(async () => { - jest.doMock('../../saved_object', () => ({ - getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), - getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ - timeline: [mockGetTemplateTimelineValue], - }), + jest.doMock('../saved_object/timelines', () => ({ + getTimelineOrNull: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineTemplateOrNull: mockGetTemplateTimeline.mockReturnValue( + mockGetTemplateTimelineValue + ), })); const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status') diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/compare_timelines_status.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/compare_timelines_status.ts index f51f9ed7f87f1..00a31aa04d1bf 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/compare_timelines_status.ts @@ -11,8 +11,8 @@ import { TimelineType, TimelineStatus, TimelineTypeLiteral, -} from '../../../../../common/types/timeline'; -import { FrameworkRequest } from '../../../framework'; +} from '../../../../common/types/timeline'; +import { FrameworkRequest } from '../../framework'; import { TimelineStatusActions, TimelineStatusAction } from './common'; import { TimelineObject } from './timeline_object'; @@ -66,7 +66,6 @@ export class CompareTimelinesStatus { version: templateTimelineInput.version, frameworkRequest, }); - this.timelineType = timelineType ?? TimelineType.default; this.title = title ?? null; this.status = status ?? TimelineStatus.active; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/default_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/default_timeline.ts similarity index 88% rename from x-pack/plugins/security_solution/server/lib/timeline/default_timeline.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/default_timeline.ts index a40a8b6365c6f..2f362bb722c63 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/default_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/default_timeline.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { Direction } from '../../graphql/types'; import { defaultHeaders } from './default_timeline_headers'; -import { SavedTimeline, TimelineType, TimelineStatus } from '../../../common/types/timeline'; +import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { Direction } from '../../../../common/search_strategy'; export const draftTimelineDefaults: SavedTimeline = { columns: defaultHeaders, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/default_timeline_headers.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/default_timeline_headers.ts similarity index 93% rename from x-pack/plugins/security_solution/server/lib/timeline/default_timeline_headers.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/default_timeline_headers.ts index 70e318982a215..d7f3ca5c008f1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/default_timeline_headers.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/default_timeline_headers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedTimeline } from '../../../common/types/timeline'; +import { SavedTimeline } from '../../../../common/types/timeline'; export const defaultColumnHeaderType = 'not-filtered'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/failure_cases.test.ts similarity index 99% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/failure_cases.test.ts index 6c9f1d0a8b590..196de3541cc23 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/failure_cases.test.ts @@ -29,7 +29,7 @@ import { TimelineStatus, TimelineType, TimelineSavedObject, -} from '../../../../../common/types/timeline'; +} from '../../../../common/types/timeline'; import { mockGetTimelineValue, mockGetTemplateTimelineValue } from '../__mocks__/import_timelines'; describe('failure cases', () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/failure_cases.ts similarity index 99% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/failure_cases.ts index cf05ca93a5167..99365a55a1d61 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/failure_cases.ts @@ -10,7 +10,7 @@ import { TimelineSavedObject, TimelineStatus, TimelineTypeLiteral, -} from '../../../../../common/types/timeline'; +} from '../../../../common/types/timeline'; export const UPDATE_TIMELINE_ERROR_MESSAGE = 'You cannot create new timelines with PATCH. Use POST instead.'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/timeline_object.ts similarity index 84% rename from x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts rename to x-pack/plugins/security_solution/server/lib/timeline/utils/timeline_object.ts index 38dda10245a0f..391f887635cff 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/timeline_object.ts @@ -10,9 +10,9 @@ import { TimelineTypeLiteral, TimelineSavedObject, TimelineStatus, -} from '../../../../../common/types/timeline'; -import { getTimeline, getTemplateTimeline } from './create_timelines'; -import { FrameworkRequest } from '../../../framework'; +} from '../../../../common/types/timeline'; +import { FrameworkRequest } from '../../framework'; +import { getTimelineOrNull, getTimelineTemplateOrNull } from '../saved_object/timelines'; interface TimelineObjectProps { id: string | null | undefined; @@ -47,10 +47,9 @@ export class TimelineObject { this.data = this.id != null ? this.type === TimelineType.template - ? await getTemplateTimeline(this.frameworkRequest, this.id) - : await getTimeline(this.frameworkRequest, this.id) + ? await getTimelineTemplateOrNull(this.frameworkRequest, this.id) + : await getTimelineOrNull(this.frameworkRequest, this.id) : null; - return this.data; } diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index a8616dc1c57d1..8b2c1126e929f 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -14,9 +14,9 @@ import { Hosts } from './hosts'; import { IndexFields } from './index_fields'; import { SourceStatus } from './source_status'; import { Sources } from './sources'; -import { Note } from './note/saved_object'; -import { PinnedEvent } from './pinned_event/saved_object'; -import { Timeline } from './timeline/saved_object'; +import { Notes } from './timeline/saved_object/notes'; +import { PinnedEvent } from './timeline/saved_object/pinned_events'; +import { Timeline } from './timeline/saved_object/timelines'; import { TotalValue, BaseHit, Explanation } from '../../common/detection_engine/types'; export * from './hosts'; @@ -31,7 +31,7 @@ export interface AppBackendLibs extends AppDomainLibs { sources: Sources; sourceStatus: SourceStatus; timeline: Timeline; - note: Note; + note: Notes; pinnedEvent: PinnedEvent; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 21724e065cb99..04f98e53ea9a3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -59,7 +59,7 @@ import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerPolicyRoutes } from './endpoint/routes/policy'; -import { ArtifactClient, EndpointArtifactClient, ManifestManager } from './endpoint/services'; +import { EndpointArtifactClient, ManifestManager } from './endpoint/services'; import { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; import { EndpointAppContext } from './endpoint/types'; import { registerDownloadArtifactRoute } from './endpoint/routes/artifacts'; @@ -352,9 +352,9 @@ export class Plugin implements IPlugin { ]); }; + const setRestoreSnapshotResponse = (response?: HttpResponse) => { + server.respondWith('POST', `${API_BASE_PATH}restore/:repository/:snapshot`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + return { setLoadRepositoriesResponse, setLoadRepositoryTypesResponse, @@ -119,6 +127,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setAddPolicyResponse, setGetPolicyResponse, setCleanupRepositoryResponse, + setRestoreSnapshotResponse, }; }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts index c0ffae81a4258..5bc970a1143a4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -25,6 +25,7 @@ const initTestBed = registerTestBed( const setupActions = (testBed: TestBed) => { const { find, component, form } = testBed; + return { findDataStreamCallout() { return find('dataStreamWarningCallOut'); @@ -37,6 +38,28 @@ const setupActions = (testBed: TestBed) => { component.update(); }, + + toggleIncludeAliases() { + act(() => { + form.toggleEuiSwitch('includeAliasesSwitch'); + }); + + component.update(); + }, + + goToStep(step: number) { + while (--step > 0) { + find('nextButton').simulate('click'); + } + component.update(); + }, + + async clickRestore() { + await act(async () => { + find('restoreButton').simulate('click'); + }); + component.update(); + }, }; }; @@ -58,5 +81,8 @@ export const setup = async (): Promise => { export type RestoreSnapshotFormTestSubject = | 'snapshotRestoreStepLogistics' | 'includeGlobalStateSwitch' + | 'includeAliasesSwitch' + | 'nextButton' + | 'restoreButton' | 'systemIndicesInfoCallOut' | 'dataStreamWarningCallOut'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts index 2fecce36f09df..9f12415b70a9f 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -33,7 +33,7 @@ describe('', () => { testBed.component.update(); }); - it('shows the data streams warning when the snapshot has data streams', () => { + test('shows the data streams warning when the snapshot has data streams', () => { const { exists } = testBed; expect(exists('dataStreamWarningCallOut')).toBe(true); }); @@ -49,7 +49,7 @@ describe('', () => { testBed.component.update(); }); - it('hides the data streams warning when the snapshot has data streams', () => { + test('hides the data streams warning when the snapshot has data streams', () => { const { exists } = testBed; expect(exists('dataStreamWarningCallOut')).toBe(false); }); @@ -65,7 +65,7 @@ describe('', () => { testBed.component.update(); }); - it('shows an info callout when include_global_state is enabled', () => { + test('shows an info callout when include_global_state is enabled', () => { const { exists, actions } = testBed; expect(exists('systemIndicesInfoCallOut')).toBe(false); @@ -75,4 +75,30 @@ describe('', () => { expect(exists('systemIndicesInfoCallOut')).toBe(true); }); }); + + // NOTE: This suite can be expanded to simulate the user setting non-default values for all of + // the form controls and asserting that the correct payload is sent to the API. + describe('include aliases', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + httpRequestsMockHelpers.setRestoreSnapshotResponse({}); + + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('is sent to the API', async () => { + const { actions } = testBed; + actions.toggleIncludeAliases(); + actions.goToStep(3); + await actions.clickRestore(); + + const expectedPayload = { includeAliases: false }; + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expectedPayload); + }); + }); }); diff --git a/x-pack/plugins/snapshot_restore/common/lib/index.ts b/x-pack/plugins/snapshot_restore/common/lib/index.ts index a375709cee7c5..fc8015c5b807b 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/index.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/index.ts @@ -6,10 +6,7 @@ */ export { flatten } from './flatten'; -export { - deserializeRestoreSettings, - serializeRestoreSettings, -} from './restore_settings_serialization'; +export { serializeRestoreSettings } from './restore_settings_serialization'; export { deserializeSnapshotDetails, deserializeSnapshotConfig, diff --git a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts index bb640000cc89a..3a78001c742ff 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - deserializeRestoreSettings, - serializeRestoreSettings, -} from './restore_settings_serialization'; +import { serializeRestoreSettings } from './restore_settings_serialization'; describe('restore_settings_serialization()', () => { it('should serialize blank restore settings', () => { @@ -56,6 +53,7 @@ describe('restore_settings_serialization()', () => { indexSettings: '{"modified_setting":123}', ignoreIndexSettings: ['setting1'], ignoreUnavailable: true, + includeAliases: true, }) ).toEqual({ indices: ['foo', 'bar'], @@ -66,6 +64,7 @@ describe('restore_settings_serialization()', () => { index_settings: { modified_setting: 123 }, ignore_index_settings: ['setting1'], ignore_unavailable: true, + include_aliases: true, }); }); @@ -76,47 +75,4 @@ describe('restore_settings_serialization()', () => { }) ).toEqual({}); }); - - it('should deserialize blank restore settings', () => { - expect(deserializeRestoreSettings({})).toEqual({}); - }); - - it('should deserialize partial restore settings', () => { - expect(deserializeRestoreSettings({})).toEqual({}); - expect( - deserializeRestoreSettings({ - indices: ['foo', 'bar'], - ignore_index_settings: ['setting1'], - partial: true, - }) - ).toEqual({ - indices: ['foo', 'bar'], - ignoreIndexSettings: ['setting1'], - partial: true, - }); - }); - - it('should deserialize full restore settings', () => { - expect( - deserializeRestoreSettings({ - indices: ['foo', 'bar'], - rename_pattern: 'capture_pattern', - rename_replacement: 'replacement_pattern', - include_global_state: true, - partial: true, - index_settings: { modified_setting: 123 }, - ignore_index_settings: ['setting1'], - ignore_unavailable: true, - }) - ).toEqual({ - indices: ['foo', 'bar'], - renamePattern: 'capture_pattern', - renameReplacement: 'replacement_pattern', - includeGlobalState: true, - partial: true, - indexSettings: '{"modified_setting":123}', - ignoreIndexSettings: ['setting1'], - ignoreUnavailable: true, - }); - }); }); diff --git a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts index 5e026246c77b9..c017bc721884c 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts @@ -26,6 +26,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest indexSettings, ignoreIndexSettings, ignoreUnavailable, + includeAliases, } = restoreSettings; let parsedIndexSettings: RestoreSettingsEs['index_settings'] | undefined; @@ -47,32 +48,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest index_settings: parsedIndexSettings, ignore_index_settings: ignoreIndexSettings, ignore_unavailable: ignoreUnavailable, - }; - - return removeUndefinedSettings(settings); -} - -export function deserializeRestoreSettings(restoreSettingsEs: RestoreSettingsEs): RestoreSettings { - const { - indices, - rename_pattern: renamePattern, - rename_replacement: renameReplacement, - include_global_state: includeGlobalState, - partial, - index_settings: indexSettings, - ignore_index_settings: ignoreIndexSettings, - ignore_unavailable: ignoreUnavailable, - } = restoreSettingsEs; - - const settings: RestoreSettings = { - indices, - renamePattern, - renameReplacement, - includeGlobalState, - partial, - indexSettings: indexSettings ? JSON.stringify(indexSettings) : undefined, - ignoreIndexSettings, - ignoreUnavailable, + include_aliases: includeAliases, }; return removeUndefinedSettings(settings); diff --git a/x-pack/plugins/snapshot_restore/common/types/restore.ts b/x-pack/plugins/snapshot_restore/common/types/restore.ts index 1bbd5cdd5a56c..9e9b91de1859e 100644 --- a/x-pack/plugins/snapshot_restore/common/types/restore.ts +++ b/x-pack/plugins/snapshot_restore/common/types/restore.ts @@ -14,6 +14,7 @@ export interface RestoreSettings { indexSettings?: string; ignoreIndexSettings?: string[]; ignoreUnavailable?: boolean; + includeAliases?: boolean; } export interface RestoreSettingsEs { @@ -25,6 +26,7 @@ export interface RestoreSettingsEs { index_settings?: { [key: string]: any }; ignore_index_settings?: string[]; ignore_unavailable?: boolean; + include_aliases?: boolean; } export interface SnapshotRestore { diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx index f672300db8821..82ace79f49f5d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx @@ -140,6 +140,7 @@ export const RestoreSnapshotForm: React.FunctionComponent = ({ iconType="arrowRight" onClick={() => onNext()} disabled={!validation.isValid} + data-test-subj="nextButton" > = ({ iconType="check" onClick={() => executeRestore()} isLoading={isSaving} + data-test-subj="restoreButton" > {isSaving ? ( = renameReplacement, partial, includeGlobalState, + includeAliases, } = restoreSettings; // States for choosing all indices, or a subset, including caching previously chosen subset list @@ -625,6 +626,41 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = /> + + {/* Include aliases */} + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={includeAliases === undefined ? true : includeAliases} + onChange={(e) => updateRestoreSettings({ includeAliases: e.target.checked })} + data-test-subj="includeAliasesSwitch" + /> + +
); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx index 4ec510a5e69a7..830b9985f86fd 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx @@ -382,7 +382,7 @@ export const PolicyTable: React.FunctionComponent = ({ > , ], diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx index 45c62f7fc57c1..3e605ade5f3c3 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx @@ -261,7 +261,7 @@ export const RepositoryTable: React.FunctionComponent = ({ > , ], diff --git a/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts b/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts deleted file mode 100644 index e9244937e48c7..0000000000000 --- a/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts +++ /dev/null @@ -1,101 +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. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.sr = components.clientAction.namespaceFactory(); - const sr = Client.prototype.sr.prototype; - - sr.policies = ca({ - urls: [ - { - fmt: '/_slm/policy', - }, - ], - method: 'GET', - }); - - sr.policy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - sr.deletePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - sr.executePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>/_execute', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - sr.updatePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - sr.executeRetention = ca({ - urls: [ - { - fmt: '/_slm/_execute_retention', - }, - ], - method: 'POST', - }); - - sr.cleanupRepository = ca({ - urls: [ - { - fmt: '/_snapshot/<%=name%>/_cleanup', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts b/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts index 5eb669b32e082..2c3ebf2e0176e 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts @@ -5,17 +5,24 @@ * 2.0. */ +import type { ElasticsearchClient } from 'src/core/server'; + // Cloud has its own system for managing SLM policies and we want to make // this clear when Snapshot and Restore is used in a Cloud deployment. // Retrieve the Cloud-managed policies so that UI can switch // logical paths based on this information. -export const getManagedPolicyNames = async (callWithInternalUser: any): Promise => { +export const getManagedPolicyNames = async ( + clusterClient: ElasticsearchClient +): Promise => { try { - const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { - filterPath: '*.*managed_policies', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await clusterClient.cluster.getSettings({ + filter_path: '*.*managed_policies', + flat_settings: true, + include_defaults: true, }); + const { 'cluster.metadata.managed_policies': managedPolicyNames = [] } = { ...defaults, ...persistent, diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts b/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts index 24960120fa3be..65dc4a750c57a 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts @@ -5,18 +5,22 @@ * 2.0. */ +import type { ElasticsearchClient } from 'src/core/server'; + // Cloud has its own system for managing snapshots and we want to make // this clear when Snapshot and Restore is used in a Cloud deployment. // Retrieve the Cloud-managed repository name so that UI can switch // logical paths based on this information. export const getManagedRepositoryName = async ( - callWithInternalUser: any + client: ElasticsearchClient ): Promise => { try { - const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { - filterPath: '*.*managed_repository', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await client.cluster.getSettings({ + filter_path: '*.*managed_repository', + flat_settings: true, + include_defaults: true, }); const { 'cluster.metadata.managed_repository': managedRepositoryName = undefined } = { ...defaults, diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index c93b5dbc4c36d..4414e3735959b 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -6,34 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { - CoreSetup, - ILegacyCustomClusterClient, - Plugin, - Logger, - PluginInitializerContext, -} from 'kibana/server'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'kibana/server'; import { PLUGIN, APP_REQUIRED_CLUSTER_PRIVILEGES } from '../common'; import { License } from './services'; import { ApiRoutes } from './routes'; import { wrapEsError } from './lib'; -import { isEsError } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch_sr'; -import type { Dependencies, SnapshotRestoreRequestHandlerContext } from './types'; +import { handleEsError } from './shared_imports'; +import type { Dependencies } from './types'; import { SnapshotRestoreConfig } from './config'; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - return core.elasticsearch.legacy.createClient('snapshotRestore', esClientConfig); -} - export class SnapshotRestoreServerPlugin implements Plugin { private readonly logger: Logger; private readonly apiRoutes: ApiRoutes; private readonly license: License; - private snapshotRestoreESClient?: ILegacyCustomClusterClient; constructor(private context: PluginInitializerContext) { const { logger } = this.context; @@ -52,7 +38,7 @@ export class SnapshotRestoreServerPlugin implements Plugin return; } - const router = http.createRouter(); + const router = http.createRouter(); this.license.setup( { @@ -82,17 +68,6 @@ export class SnapshotRestoreServerPlugin implements Plugin ], }); - http.registerRouteHandlerContext( - 'snapshotRestore', - async (ctx, request) => { - this.snapshotRestoreESClient = - this.snapshotRestoreESClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.snapshotRestoreESClient.asScoped(request), - }; - } - ); - this.apiRoutes.setup({ router, license: this.license, @@ -102,17 +77,11 @@ export class SnapshotRestoreServerPlugin implements Plugin isSlmEnabled: pluginConfig.slm_ui.enabled, }, lib: { - isEsError, + handleEsError, wrapEsError, }, }); } public start() {} - - public stop() { - if (this.snapshotRestoreESClient) { - this.snapshotRestoreESClient.close(); - } - } } diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts index c9ee33c1d387b..217bce9721f63 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts @@ -27,12 +27,12 @@ export function registerAppRoutes({ router, config: { isSecurityEnabled }, license, - lib: { isEsError }, + lib: { handleEsError }, }: RouteDependencies) { router.get( { path: addBasePath('privileges'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const privilegesResult: Privileges = { hasAllPrivileges: true, @@ -48,42 +48,36 @@ export function registerAppRoutes({ } try { - // Get cluster priviliges - const { has_all_requested: hasAllPrivileges, cluster } = await callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES], - }, - } - ); + // Get cluster privileges + const { + body: { has_all_requested: hasAllPrivileges, cluster }, + } = await clusterClient.asCurrentUser.security.hasPrivileges({ + body: { + cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES], + }, + }); // Find missing cluster privileges and set overall app privileges privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); privilegesResult.hasAllPrivileges = hasAllPrivileges; // Get all index privileges the user has - const { indices } = await callAsCurrentUser('transport.request', { - path: '/_security/user/_privileges', - method: 'GET', - }); + const { + body: { indices }, + } = await clusterClient.asCurrentUser.security.getUserPrivileges(); // Check if they have all the required index privileges for at least one index - const oneIndexWithAllPrivileges = indices.find( - ({ privileges }: { privileges: string[] }) => { - if (privileges.includes('all')) { - return true; - } + const oneIndexWithAllPrivileges = indices.find(({ privileges }) => { + if (privileges.includes('all')) { + return true; + } - const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) => - privileges.includes(privilege) - ); + const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) => + privileges.includes(privilege) + ); - return indexHasAllPrivileges; - } - ); + return indexHasAllPrivileges; + }); // If they don't, return list of required index privileges if (!oneIndexWithAllPrivileges) { @@ -92,14 +86,7 @@ export function registerAppRoutes({ return res.ok({ body: privilegesResult }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index ff66e020d2224..5ef5f2d01b96c 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -44,12 +44,23 @@ describe('[Snapshot and Restore API Routes] Policy', () => { isManagedPolicy: false, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const putClusterSettingsFn = router.getMockApiFn('cluster.putSettings'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const putLifecycleFn = router.getMockApiFn('slm.putLifecycle'); + const executeLifecycleFn = router.getMockApiFn('slm.executeLifecycle'); + const deleteLifecycleFn = router.getMockApiFn('slm.deleteLifecycle'); + const resolveIndicesFn = router.getMockApiFn('indices.resolveIndex'); beforeAll(() => { registerPolicyRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -64,7 +75,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { fooPolicy: mockEsPolicy, barPolicy: mockEsPolicy, }; - router.callAsCurrentUserResponses = [[], mockEsResponse]; + getClusterSettingsFn.mockResolvedValue({ body: {} }); + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { policies: [ { @@ -84,7 +96,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return empty array if no repositories returned from ES', async () => { const mockEsResponse = {}; - router.callAsCurrentUserResponses = [[], mockEsResponse]; + getClusterSettingsFn.mockResolvedValue({ body: {} }); + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { policies: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse, @@ -92,11 +105,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error()), // Get managed policyNames will silently fail - jest.fn().mockRejectedValueOnce(new Error()), // Call to 'sr.policies' - ]; - + getClusterSettingsFn.mockRejectedValue(new Error()); // Get managed policyNames should silently fail + getLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -116,7 +126,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { [name]: mockEsPolicy, }; - router.callAsCurrentUserResponses = [mockEsResponse, {}]; + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); + getClusterSettingsFn.mockResolvedValue({ body: {} }); const expectedResponse = { policy: { @@ -130,14 +141,20 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should return 404 error if not returned from ES', async () => { - router.callAsCurrentUserResponses = [{}, {}]; + getLifecycleFn.mockRejectedValue({ + name: 'ResponseError', + body: {}, + statusCode: 404, + }); + getClusterSettingsFn.mockResolvedValue({}); const response = await router.runRequest(mockRequest); - expect(response.status).toBe(404); + expect(response.statusCode).toBe(404); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockRejectedValueOnce(new Error('something unexpected')); + getClusterSettingsFn.mockResolvedValueOnce({ body: {} }); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -158,7 +175,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { const mockEsResponse = { snapshot_name: 'foo-policy-snapshot', }; - router.callAsCurrentUserResponses = [mockEsResponse]; + executeLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { snapshotName: 'foo-policy-snapshot', @@ -170,7 +187,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + executeLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -189,7 +206,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: names, errors: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -202,10 +219,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteLifecycleFn.mockRejectedValue(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -228,10 +242,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteLifecycleFn.mockRejectedValueOnce(mockEsError); + deleteLifecycleFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [names[1]], @@ -264,7 +276,9 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{}, mockEsResponse]; + + getLifecycleFn.mockResolvedValue({ body: {} }); + putLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -274,14 +288,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return error if policy with the same name already exists', async () => { const mockEsResponse = { [name]: {} }; - router.callAsCurrentUserResponses = [mockEsResponse]; + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const response = await router.runRequest(mockRequest); expect(response.status).toBe(409); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockResolvedValue({ body: {} }); + putLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -302,14 +317,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse]; + getLifecycleFn.mockResolvedValue({ body: { [name]: {} } }); + putLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -343,7 +359,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }, ], }; - router.callAsCurrentUserResponses = [mockEsResponse]; + + resolveIndicesFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { indices: ['fooIndex'], @@ -358,14 +375,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => { aliases: [], data_streams: [], }; - router.callAsCurrentUserResponses = [mockEsResponse]; + resolveIndicesFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { indices: [], dataStreams: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + resolveIndicesFn.mockRejectedValueOnce(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -383,14 +400,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse]; + putClusterSettingsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + putClusterSettingsFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index fa127880fd806..77264c4bffc9a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { PutSnapshotLifecycleRequest } from '@elastic/elasticsearch/api/types'; import { schema, TypeOf } from '@kbn/config-schema'; import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types'; @@ -17,21 +17,19 @@ import { nameParameterSchema, policySchema } from './validate_schemas'; export function registerPolicyRoutes({ router, license, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all policies router.get( { path: addBasePath('policies'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; - const managedPolicies = await getManagedPolicyNames(callAsCurrentUser); + const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser); try { // Get policies - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policies', { + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ human: true, }); @@ -39,19 +37,14 @@ export function registerPolicyRoutes({ return res.ok({ body: { policies: Object.entries(policiesByName).map(([name, policy]) => { - return deserializePolicy(name, policy, managedPolicies); + // TODO: Figure out why our {@link SlmPolicyEs} is not compatible with: + // import type { SnapshotLifecyclePolicyMetadata } from '@elastic/elasticsearch/api/types'; + return deserializePolicy(name, policy as SlmPolicyEs, managedPolicies); }), }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -60,39 +53,25 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policy/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policy', { - name, + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ + policy_id: name, human: true, }); - if (!policiesByName[name]) { - // If policy doesn't exist, ES will return 200 with an empty object, so manually throw 404 here - return res.notFound({ body: 'Policy not found' }); - } - - const managedPolicies = await getManagedPolicyNames(callAsCurrentUser); + const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser); // Deserialize policy return res.ok({ body: { - policy: deserializePolicy(name, policiesByName[name], managedPolicies), + policy: deserializePolicy(name, policiesByName[name] as SlmPolicyEs, managedPolicies), }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -101,13 +80,17 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policies'), validate: { body: policySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; + const policy = req.body as TypeOf; const { name } = policy; try { // Check that policy with the same name doesn't already exist - const policyByName = await callAsCurrentUser('sr.policy', { name }); + const { body: policyByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ + policy_id: name, + }); + if (policyByName[name]) { return res.conflict({ body: 'There is already a policy with that name.' }); } @@ -117,21 +100,15 @@ export function registerPolicyRoutes({ try { // Otherwise create new policy - const response = await callAsCurrentUser('sr.updatePolicy', { - name, - body: serializePolicy(policy), + const response = await clusterClient.asCurrentUser.slm.putLifecycle({ + policy_id: name, + // TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']} + body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -143,31 +120,25 @@ export function registerPolicyRoutes({ validate: { params: nameParameterSchema, body: policySchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const policy = req.body as TypeOf; try { // Check that policy with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callAsCurrentUser('sr.policy', { name }); + await clusterClient.asCurrentUser.slm.getLifecycle({ policy_id: name }); // Otherwise update policy - const response = await callAsCurrentUser('sr.updatePolicy', { - name, - body: serializePolicy(policy), + const response = await clusterClient.asCurrentUser.slm.putLifecycle({ + policy_id: name, + // TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']} + body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -176,7 +147,7 @@ export function registerPolicyRoutes({ router.delete( { path: addBasePath('policies/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const policyNames = name.split(','); @@ -187,7 +158,8 @@ export function registerPolicyRoutes({ await Promise.all( policyNames.map((policyName) => { - return callAsCurrentUser('sr.deletePolicy', { name: policyName }) + return clusterClient.asCurrentUser.slm + .deleteLifecycle({ policy_id: policyName }) .then(() => response.itemsDeleted.push(policyName)) .catch((e) => response.errors.push({ @@ -206,23 +178,18 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policy/{name}/run'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const { snapshot_name: snapshotName } = await callAsCurrentUser('sr.executePolicy', { - name, + const { + body: { snapshot_name: snapshotName }, + } = await clusterClient.asCurrentUser.slm.executeLifecycle({ + policy_id: name, }); return res.ok({ body: { snapshotName } }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -231,19 +198,14 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policies/indices'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; try { - const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/*`, - query: { - expand_wildcards: 'all', - }, - } - ); + const response = await clusterClient.asCurrentUser.indices.resolveIndex({ + name: '*', + expand_wildcards: 'all', + }); + const resolvedIndicesResponse = response.body as ResolveIndexResponseFromES; const body: PolicyIndicesResponse = { dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(), @@ -256,14 +218,7 @@ export function registerPolicyRoutes({ body, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -272,18 +227,21 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policies/retention_settings'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', { - filterPath: '**.slm.retention*', - includeDefaults: true, + const { client: clusterClient } = ctx.core.elasticsearch; + const { + body: { persistent, transient, defaults }, + } = await clusterClient.asCurrentUser.cluster.getSettings({ + filter_path: '**.slm.retention*', + include_defaults: true, }); - const { slm: retentionSettings = undefined } = { + const { slm: retentionSettings }: { slm?: { retention_schedule: string } } = { ...defaults, ...persistent, ...transient, }; - const { retention_schedule: retentionSchedule } = retentionSettings; + const retentionSchedule = + retentionSettings != null ? retentionSettings.retention_schedule : undefined; return res.ok({ body: { retentionSchedule }, @@ -300,11 +258,11 @@ export function registerPolicyRoutes({ validate: { body: retentionSettingsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { retentionSchedule } = req.body as TypeOf; try { - const response = await callAsCurrentUser('cluster.putSettings', { + const response = await clusterClient.asCurrentUser.cluster.putSettings({ body: { persistent: { slm: { @@ -314,16 +272,9 @@ export function registerPolicyRoutes({ }, }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -332,9 +283,9 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policies/retention'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const response = await callAsCurrentUser('sr.executeRetention'); - return res.ok({ body: response }); + const { client: clusterClient } = ctx.core.elasticsearch; + const response = await clusterClient.asCurrentUser.slm.executeRetention(); + return res.ok({ body: response.body }); }) ); } diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts index 35dce2c5d558f..7d14d62bfe1a0 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -19,12 +19,25 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const clusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const createRepoFn = router.getMockApiFn('snapshot.createRepository'); + const getRepoFn = router.getMockApiFn('snapshot.getRepository'); + const deleteRepoFn = router.getMockApiFn('snapshot.deleteRepository'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const getSnapshotFn = router.getMockApiFn('snapshot.get'); + const verifyRepoFn = router.getMockApiFn('snapshot.verifyRepository'); + const catPluginsFn = router.getMockApiFn('cat.plugins'); beforeAll(() => { registerRepositoriesRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -48,11 +61,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockRepositoryEsResponse, - mockPolicyEsResponse, - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse }); const expectedResponse = { repositories: [ @@ -85,11 +96,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockRepositoryEsResponse, - mockPolicyEsResponse, - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse }); const expectedResponse = { repositories: [], @@ -103,10 +112,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - jest.fn().mockRejectedValueOnce(new Error()), - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -128,11 +135,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { [name]: { type: '', settings: {} }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - {}, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: {} }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -144,7 +149,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return empty repository object if not returned from ES', async () => { - router.callAsCurrentUserResponses = [mockSnapshotGetManagedRepositoryEsResponse, {}, {}]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: {} }); + getSnapshotFn.mockResolvedValue({ body: {} }); const expectedResponse = { repository: {}, @@ -167,11 +174,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - mockEsSnapshotResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotResponse }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -190,11 +195,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }; const mockEsSnapshotError = jest.fn().mockRejectedValueOnce(new Error('snapshot error')); - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - mockEsSnapshotError, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotError }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -208,10 +211,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - jest.fn().mockRejectedValueOnce(new Error()), - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -230,7 +232,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return repository verification response if returned from ES', async () => { const mockEsResponse = { nodes: {} }; - router.callAsCurrentUserResponses = [mockEsResponse]; + verifyRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { verification: { valid: true, response: mockEsResponse }, @@ -241,7 +243,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return repository verification error if returned from ES', async () => { const mockEsResponse = { error: {}, status: 500 }; - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(mockEsResponse)]; + verifyRepoFn.mockRejectedValueOnce(mockEsResponse); const expectedResponse = { verification: { valid: false, error: mockEsResponse }, @@ -258,7 +260,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }; it('should return default types if no repository plugins returned from ES', async () => { - router.callAsCurrentUserResponses = [{}]; + catPluginsFn.mockResolvedValue({ body: {} }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES]; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -269,7 +271,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { const pluginTypes = Object.entries(REPOSITORY_PLUGINS_MAP).map(([key, value]) => value); const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))]; - router.callAsCurrentUserResponses = [mockEsResponse]; + catPluginsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES, ...pluginTypes]; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -278,7 +280,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should not return non-repository plugins returned from ES', async () => { const pluginNames = ['foo-plugin', 'bar-plugin']; const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))]; - router.callAsCurrentUserResponses = [mockEsResponse]; + catPluginsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES]; @@ -286,11 +288,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error('Error getting pluggins')), - ]; + catPluginsFn.mockRejectedValueOnce(new Error('Error getting plugins')); - await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting pluggins'); + await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting plugins'); }); }); @@ -307,7 +307,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{}, mockEsResponse]; + getRepoFn.mockResolvedValue({ body: {} }); + createRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; @@ -315,15 +316,15 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return error if repository with the same name already exists', async () => { - router.callAsCurrentUserResponses = [{ [name]: {} }]; - + getRepoFn.mockResolvedValue({ body: { [name]: {} } }); const response = await router.runRequest(mockRequest); expect(response.status).toBe(409); }); it('should throw if ES error', async () => { const error = new Error('Oh no!'); - router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(error)]; + getRepoFn.mockResolvedValue({ body: {} }); + createRepoFn.mockRejectedValue(error); await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); }); @@ -344,7 +345,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse]; + getRepoFn.mockResolvedValue({ body: { [name]: {} } }); + createRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = mockEsResponse; @@ -352,7 +354,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -369,7 +371,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse }); + deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: names, errors: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -380,10 +383,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteRepoFn.mockRejectedValueOnce(mockEsError); + deleteRepoFn.mockRejectedValueOnce(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -402,11 +403,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; + const responses = [Promise.reject(mockEsError), Promise.resolve({ body: mockEsResponse })]; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteRepoFn.mockImplementation(() => responses.shift()); const expectedResponse = { itemsDeleted: [names[1]], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index c9945bb172e6c..96099e3fbb1eb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -6,9 +6,10 @@ */ import { TypeOf } from '@kbn/config-schema'; +import type { SnapshotRepositorySettings } from '@elastic/elasticsearch/api/types'; import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants'; -import { Repository, RepositoryType, SlmPolicyEs } from '../../../common/types'; +import { Repository, RepositoryType } from '../../../common/types'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; import { nameParameterSchema, repositorySchema } from './validate_schemas'; @@ -28,21 +29,23 @@ export function registerRepositoriesRoutes({ router, license, config: { isCloudEnabled }, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all repositories router.get( { path: addBasePath('repositories'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const managedRepositoryName = await getManagedRepositoryName(callAsCurrentUser); + const { client: clusterClient } = ctx.core.elasticsearch; + const managedRepositoryName = await getManagedRepositoryName(clusterClient.asCurrentUser); let repositoryNames: string[] | undefined; let repositories: Repository[]; let managedRepository: ManagedRepository; try { - const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', { + const { + body: repositoriesByName, + } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: '_all', }); repositoryNames = Object.keys(repositoriesByName); @@ -52,29 +55,20 @@ export function registerRepositoriesRoutes({ name, type, settings: deserializeRepositorySettings(settings), - }; + } as Repository; }); managedRepository = { name: managedRepositoryName, }; } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } // If a managed repository, we also need to check if a policy is associated to it if (managedRepositoryName) { try { - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policies', { + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ human: true, }); @@ -102,45 +96,28 @@ export function registerRepositoriesRoutes({ router.get( { path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); let repositoryByName: any; try { - repositoryByName = await callAsCurrentUser('snapshot.getRepository', { + ({ body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name, - }); + })); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } - const { - responses: snapshotResponses, - }: { - responses: Array<{ - repository: string; - snapshots: any[]; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository: name, snapshot: '_all', - }).catch((e) => ({ - responses: [ - { - snapshots: null, - }, - ], - })); + }); + + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: snapshotResponses } = response.body; if (repositoryByName[name]) { const { type = '', settings = {} } = repositoryByName[name]; @@ -176,18 +153,20 @@ export function registerRepositoriesRoutes({ router.get( { path: addBasePath('repository_types'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; // In ECE/ESS, do not enable the default types const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES]; try { // Call with internal user so that the requesting user does not need `monitoring` cluster // privilege just to see list of available repository types - const plugins: any[] = await callAsCurrentUser('cat.plugins', { format: 'json' }); + const { body: plugins } = await clusterClient.asCurrentUser.cat.plugins({ format: 'json' }); // Filter list of plugins to repository-related ones if (plugins && plugins.length) { - const pluginNames: string[] = [...new Set(plugins.map((plugin) => plugin.component))]; + const pluginNames: string[] = [ + ...new Set(plugins.map((plugin) => plugin.component ?? '')), + ]; pluginNames.forEach((pluginName) => { if (REPOSITORY_PLUGINS_MAP[pluginName]) { types.push(REPOSITORY_PLUGINS_MAP[pluginName]); @@ -196,14 +175,7 @@ export function registerRepositoriesRoutes({ } return res.ok({ body: types }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -215,20 +187,24 @@ export function registerRepositoriesRoutes({ validate: { params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const verificationResults = await callAsCurrentUser('snapshot.verifyRepository', { - repository: name, - }).catch((e) => ({ - valid: false, - error: e.response ? JSON.parse(e.response) : e, - })); + const { body: verificationResults } = await clusterClient.asCurrentUser.snapshot + .verifyRepository({ + repository: name, + }) + .catch((e) => ({ + body: { + valid: false, + error: e.response ? JSON.parse(e.response) : e, + }, + })); return res.ok({ body: { - verification: verificationResults.error + verification: (verificationResults as { error?: Error }).error ? verificationResults : { valid: true, @@ -237,14 +213,7 @@ export function registerRepositoriesRoutes({ }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -256,20 +225,24 @@ export function registerRepositoriesRoutes({ validate: { params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const cleanupResults = await callAsCurrentUser('sr.cleanupRepository', { - name, - }).catch((e) => ({ - cleaned: false, - error: e.response ? JSON.parse(e.response) : e, - })); + const { body: cleanupResults } = await clusterClient.asCurrentUser.snapshot + .cleanupRepository({ + repository: name, + }) + .catch((e) => ({ + body: { + cleaned: false, + error: e.response ? JSON.parse(e.response) : e, + }, + })); return res.ok({ body: { - cleanup: cleanupResults.error + cleanup: (cleanupResults as { error?: Error }).error ? cleanupResults : { cleaned: true, @@ -278,14 +251,7 @@ export function registerRepositoriesRoutes({ }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -294,14 +260,16 @@ export function registerRepositoriesRoutes({ router.put( { path: addBasePath('repositories'), validate: { body: repositorySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name = '', type = '', settings = {} } = req.body as TypeOf; // Check that repository with the same name doesn't already exist try { - const repositoryByName = await callAsCurrentUser('snapshot.getRepository', { - repository: name, - }); + const { body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository( + { + repository: name, + } + ); if (repositoryByName[name]) { return res.conflict({ body: 'There is already a repository with that name.' }); } @@ -311,25 +279,19 @@ export function registerRepositoriesRoutes({ // Otherwise create new repository try { - const response = await callAsCurrentUser('snapshot.createRepository', { + const response = await clusterClient.asCurrentUser.snapshot.createRepository({ repository: name, body: { type, - settings: serializeRepositorySettings(settings), + // TODO: Bring {@link RepositorySettings} in line with {@link SnapshotRepositorySettings} + settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings, }, verify: false, }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -341,37 +303,30 @@ export function registerRepositoriesRoutes({ validate: { body: repositorySchema, params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const { type = '', settings = {} } = req.body as TypeOf; try { // Check that repository with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callAsCurrentUser('snapshot.getRepository', { repository: name }); + await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name }); // Otherwise update repository - const response = await callAsCurrentUser('snapshot.createRepository', { + const response = await clusterClient.asCurrentUser.snapshot.createRepository({ repository: name, body: { type, - settings: serializeRepositorySettings(settings), + settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings, }, verify: false, }); return res.ok({ - body: response, + body: response.body, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -380,7 +335,7 @@ export function registerRepositoriesRoutes({ router.delete( { path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const repositoryNames = name.split(','); @@ -392,7 +347,8 @@ export function registerRepositoriesRoutes({ try { await Promise.all( repositoryNames.map((repoName) => { - return callAsCurrentUser('snapshot.deleteRepository', { repository: repoName }) + return clusterClient.asCurrentUser.snapshot + .deleteRepository({ repository: repoName }) .then(() => response.itemsDeleted.push(repoName)) .catch((e) => response.errors.push({ @@ -405,14 +361,7 @@ export function registerRepositoriesRoutes({ return res.ok({ body: response }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts index fe33331522daa..a6f6924aaae31 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts @@ -17,15 +17,21 @@ describe('[Snapshot and Restore API Routes] Restore', () => { index: { size: {}, files: {} }, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); beforeAll(() => { registerRestoreRoutes({ - router: router as any, ...routeDependencies, + router, }); }); + /** + * ES APIs used by these endpoints + */ + const indicesRecoveryFn = router.getMockApiFn('indices.recovery'); + const restoreSnapshotFn = router.getMockApiFn('snapshot.restore'); + describe('Restore snapshot', () => { const mockRequest: RequestMock = { method: 'post', @@ -39,7 +45,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should return successful response from ES', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse]; + restoreSnapshotFn.mockResolvedValue({ body: mockEsResponse }); await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: mockEsResponse, @@ -47,7 +53,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + restoreSnapshotFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -76,7 +82,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }, }; - router.callAsCurrentUserResponses = [mockEsResponse]; + indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [ { @@ -100,7 +106,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should return empty array if no repositories returned from ES', async () => { const mockEsResponse = {}; - router.callAsCurrentUserResponses = [mockEsResponse]; + indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse: any[] = []; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -109,7 +115,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + indicesRecoveryFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts index c4300bafc75fb..b7281fee04c53 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts @@ -6,6 +6,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { RestoreRequest } from '@elastic/elasticsearch/api/types'; import { SnapshotRestore, SnapshotRestoreShardEs } from '../../../common/types'; import { serializeRestoreSettings } from '../../../common/lib'; @@ -14,20 +15,20 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; import { restoreSettingsSchema } from './validate_schemas'; -export function registerRestoreRoutes({ router, license, lib: { isEsError } }: RouteDependencies) { +export function registerRestoreRoutes({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { // GET all snapshot restores router.get( { path: addBasePath('restores'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; try { const snapshotRestores: SnapshotRestore[] = []; - const recoveryByIndexName: { - [key: string]: { - shards: SnapshotRestoreShardEs[]; - }; - } = await callAsCurrentUser('indices.recovery', { + const { body: recoveryByIndexName } = await clusterClient.asCurrentUser.indices.recovery({ human: true, }); @@ -40,7 +41,8 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R .filter((shard) => shard.type === 'SNAPSHOT') .sort((a, b) => a.id - b.id) .map((shard) => { - const deserializedShard = deserializeRestoreShard(shard); + // TODO: Bring {@link SnapshotRestoreShardEs} in line with {@link ShardRecovery} + const deserializedShard = deserializeRestoreShard(shard as SnapshotRestoreShardEs); const { startTimeInMillis, stopTimeInMillis } = deserializedShard; // Set overall latest activity time @@ -80,14 +82,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R return res.ok({ body: snapshotRestores }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -104,27 +99,21 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R validate: { body: restoreSettingsSchema, params: restoreParamsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { repository, snapshot } = req.params as TypeOf; const restoreSettings = req.body as TypeOf; try { - const response = await callAsCurrentUser('snapshot.restore', { + const response = await clusterClient.asCurrentUser.snapshot.restore({ repository, snapshot, - body: serializeRestoreSettings(restoreSettings), + // TODO: Bring {@link RestoreSettingsEs} in line with {@link RestoreRequest['body']} + body: serializeRestoreSettings(restoreSettings) as RestoreRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index 97eb34b4aaa73..bd7dffe987feb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -29,12 +29,21 @@ const defaultSnapshot = { }; describe('[Snapshot and Restore API Routes] Snapshots', () => { - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const getRepoFn = router.getMockApiFn('snapshot.getRepository'); + const getSnapshotFn = router.getMockApiFn('snapshot.get'); + const deleteSnapshotFn = router.getMockApiFn('snapshot.delete'); beforeAll(() => { registerSnapshotsRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -60,31 +69,29 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { barRepository: {}, }; - const mockGetSnapshotsFooResponse = Promise.resolve({ + const mockGetSnapshotsFooResponse = { responses: [ { repository: 'fooRepository', snapshots: [{ snapshot: 'snapshot1' }], }, ], - }); + }; - const mockGetSnapshotsBarResponse = Promise.resolve({ + const mockGetSnapshotsBarResponse = { responses: [ { repository: 'barRepository', snapshots: [{ snapshot: 'snapshot2' }], }, ], - }); + }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetPolicyEsResponse, - mockSnapshotGetRepositoryEsResponse, - mockGetSnapshotsFooResponse, - mockGetSnapshotsBarResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse }); + getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsFooResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsBarResponse }); const expectedResponse = { errors: {}, @@ -120,11 +127,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { const mockSnapshotGetPolicyEsResponse = {}; const mockSnapshotGetRepositoryEsResponse = {}; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetPolicyEsResponse, - mockSnapshotGetRepositoryEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse }); + getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse }); const expectedResponse = { errors: [], @@ -138,11 +143,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { }); test('throws if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error('Error getting managed repository')), - jest.fn().mockRejectedValueOnce(new Error('Error getting policies')), - jest.fn().mockRejectedValueOnce(new Error('Error getting repository')), - ]; + getClusterSettingsFn.mockRejectedValueOnce(new Error()); + getLifecycleFn.mockRejectedValueOnce(new Error()); + getRepoFn.mockRejectedValueOnce(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -177,10 +180,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse }); const expectedResponse = { ...defaultSnapshot, @@ -215,12 +216,13 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse }); - await expect(router.runRequest(mockRequest)).rejects.toThrowError(); + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: 'Snapshot not found', + status: 404, + }); }); }); @@ -243,7 +245,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [ @@ -261,10 +264,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -289,10 +290,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [{ snapshot: 'snapshot-2', repository: 'barRepository' }], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 03e3b4ecc0887..8f6f44f63a556 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -6,31 +6,31 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { RouteDependencies } from '../../types'; -import { addBasePath } from '../helpers'; -import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; +import type { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; import { deserializeSnapshotDetails } from '../../../common/lib'; +import type { RouteDependencies } from '../../types'; import { getManagedRepositoryName } from '../../lib'; +import { addBasePath } from '../helpers'; export function registerSnapshotsRoutes({ router, license, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all snapshots router.get( { path: addBasePath('snapshots'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); let policies: string[] = []; // Attempt to retrieve policies // This could fail if user doesn't have access to read SLM policies try { - const policiesByName = await callAsCurrentUser('sr.policies'); + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle(); policies = Object.keys(policiesByName); } catch (e) { // Silently swallow error as policy names aren't required in UI @@ -44,7 +44,9 @@ export function registerSnapshotsRoutes({ let repositoryNames: string[]; try { - const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', { + const { + body: repositoriesByName, + } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: '_all', }); repositoryNames = Object.keys(repositoriesByName); @@ -55,13 +57,7 @@ export function registerSnapshotsRoutes({ }); } } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - throw e; + return handleEsError({ error: e, response: res }); } const snapshots: SnapshotDetails[] = []; @@ -71,23 +67,27 @@ export function registerSnapshotsRoutes({ const fetchSnapshotsForRepository = async (repository: string) => { try { // If any of these repositories 504 they will cost the request significant time. - const { - responses: fetchedResponses, - }: { - responses: Array<{ - repository: 'string'; - snapshots: SnapshotDetailsEs[]; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository, snapshot: '_all', ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. }); + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: fetchedResponses } = response.body; + // Decorate each snapshot with the repository with which it's associated. + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => { + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client fetchedSnapshots.forEach((snapshot) => { - snapshots.push(deserializeSnapshotDetails(repository, snapshot, managedRepository)); + snapshots.push( + deserializeSnapshotDetails( + repository, + snapshot as SnapshotDetailsEs, + managedRepository + ) + ); }); }); @@ -124,28 +124,27 @@ export function registerSnapshotsRoutes({ validate: { params: getOneParamsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { repository, snapshot } = req.params as TypeOf; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); try { - const { - responses: snapshotsResponse, - }: { - responses: Array<{ - repository: string; - snapshots: SnapshotDetailsEs[]; - error?: any; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository, snapshot: '_all', ignore_unavailable: true, }); + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: snapshotsResponse } = response.body; + const snapshotsList = snapshotsResponse && snapshotsResponse[0] && snapshotsResponse[0].snapshots; + if (!snapshotsList || snapshotsList.length === 0) { + return res.notFound({ body: 'Snapshot not found' }); + } const selectedSnapshot = snapshotsList.find( + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client ({ snapshot: snapshotName }) => snapshot === snapshotName ) as SnapshotDetailsEs; @@ -155,10 +154,12 @@ export function registerSnapshotsRoutes({ } const successfulSnapshots = snapshotsList + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client .filter(({ state }) => state === 'SUCCESS') + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client .sort((a, b) => { - return +new Date(b.end_time) - +new Date(a.end_time); - }); + return +new Date(b.end_time!) - +new Date(a.end_time!); + }) as SnapshotDetailsEs[]; return res.ok({ body: deserializeSnapshotDetails( @@ -169,14 +170,7 @@ export function registerSnapshotsRoutes({ ), }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -192,7 +186,7 @@ export function registerSnapshotsRoutes({ router.post( { path: addBasePath('snapshots/bulk_delete'), validate: { body: deleteSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const response: { itemsDeleted: Array<{ snapshot: string; repository: string }>; @@ -210,7 +204,8 @@ export function registerSnapshotsRoutes({ for (let i = 0; i < snapshots.length; i++) { const { snapshot, repository } = snapshots[i]; - await callAsCurrentUser('snapshot.delete', { snapshot, repository }) + await clusterClient.asCurrentUser.snapshot + .delete({ snapshot, repository }) .then(() => response.itemsDeleted.push({ snapshot, repository })) .catch((e) => response.errors.push({ @@ -222,14 +217,7 @@ export function registerSnapshotsRoutes({ return res.ok({ body: response }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts index fe156f6ba9750..af31466c2cefe 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts @@ -176,4 +176,5 @@ export const restoreSettingsSchema = schema.object({ indexSettings: schema.maybe(schema.string()), ignoreIndexSettings: schema.maybe(schema.arrayOf(schema.string())), ignoreUnavailable: schema.maybe(schema.boolean()), + includeAliases: schema.maybe(schema.boolean()), }); diff --git a/x-pack/plugins/snapshot_restore/server/services/license.ts b/x-pack/plugins/snapshot_restore/server/services/license.ts index 93cf86eae5359..e209edcd899b4 100644 --- a/x-pack/plugins/snapshot_restore/server/services/license.ts +++ b/x-pack/plugins/snapshot_restore/server/services/license.ts @@ -6,11 +6,15 @@ */ import { Logger } from 'src/core/server'; -import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'kibana/server'; +import type { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; import { LicensingPluginSetup } from '../../../licensing/server'; import { LicenseType } from '../../../licensing/common/types'; -import type { SnapshotRestoreRequestHandlerContext } from '../types'; export interface LicenseStatus { isValid: boolean; @@ -51,13 +55,11 @@ export class License { }); } - guardApiRoute( - handler: RequestHandler - ) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( - ctx: Context, + ctx: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory ) { diff --git a/x-pack/plugins/snapshot_restore/server/shared_imports.ts b/x-pack/plugins/snapshot_restore/server/shared_imports.ts index df9b3dd53cc1f..7f55d189457c7 100644 --- a/x-pack/plugins/snapshot_restore/server/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/server/shared_imports.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts index 6bd7c10497b24..77c8ab4759b53 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts @@ -6,13 +6,14 @@ */ import { License } from '../../services'; +import { handleEsError } from '../../shared_imports'; import { wrapEsError } from '../../lib'; -import { isEsError } from '../../shared_imports'; +import type { RouteDependencies } from '../../types'; const license = new License(); license.getStatus = jest.fn().mockReturnValue({ isValid: true }); -export const routeDependencies = { +export const routeDependencies: Omit = { license, config: { isSecurityEnabled: jest.fn().mockReturnValue(true), @@ -20,7 +21,7 @@ export const routeDependencies = { isSlmEnabled: true, }, lib: { - isEsError, wrapEsError, + handleEsError, }, }; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts index 656301abc5358..efd0ebd0fd1c4 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { set } from '@elastic/safer-lodash-set'; +import type { IRouter } from 'src/core/server'; +import { get } from 'lodash'; + +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; type RequestHandler = (...params: any[]) => any; @@ -48,7 +51,7 @@ export interface RequestMock { [key: string]: any; } -export class RouterMock { +export class RouterMock implements IRouter { /** * Cache to keep a reference to all the request handler defined on the router for each HTTP method and path */ @@ -60,15 +63,13 @@ export class RouterMock { patch: {}, }; - private _callAsCurrentUserCallCount = 0; - private _callAsCurrentUserResponses: any[] = []; - private contextMock = {}; + public contextMock = { + core: { elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient() } }, + }; - constructor(pathToESclient = 'core.elasticsearch.dataClient') { - set(this.contextMock, pathToESclient, { - callAsCurrentUser: this.callAsCurrentUser.bind(this), - }); - } + getRoutes = jest.fn(); + handleLegacyErrors = jest.fn(); + routerPath = ''; get({ path }: { path: string }, handler: RequestHandler) { this.cacheHandlers.get[path] = handler; @@ -90,17 +91,8 @@ export class RouterMock { this.cacheHandlers.patch[path] = handler; } - private callAsCurrentUser() { - const index = this._callAsCurrentUserCallCount; - this._callAsCurrentUserCallCount += 1; - const response = this._callAsCurrentUserResponses[index]; - - return typeof response === 'function' ? Promise.resolve(response()) : Promise.resolve(response); - } - - public set callAsCurrentUserResponses(responses: any[]) { - this._callAsCurrentUserCallCount = 0; - this._callAsCurrentUserResponses = responses; + getMockApiFn(path: string): jest.Mock { + return get(this.contextMock.core.elasticsearch.client.asCurrentUser, path); } runRequest({ method, path, ...mockRequest }: RequestMock) { diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index c92de645aa2de..8c2ad74865e45 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -5,19 +5,14 @@ * 2.0. */ -import type { - LegacyScopedClusterClient, - ILegacyScopedClusterClient, - IRouter, - RequestHandlerContext, -} from 'src/core/server'; +import type { IRouter, RequestHandlerContext, IScopedClusterClient } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { CloudSetup } from '../../cloud/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; import { wrapEsError } from './lib'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; @@ -27,7 +22,7 @@ export interface Dependencies { } export interface RouteDependencies { - router: SnapshotRestoreRouter; + router: IRouter; license: License; config: { isSlmEnabled: boolean; @@ -35,8 +30,8 @@ export interface RouteDependencies { isCloudEnabled: boolean; }; lib: { - isEsError: typeof isEsError; wrapEsError: typeof wrapEsError; + handleEsError: typeof handleEsError; }; } @@ -56,13 +51,13 @@ export interface ResolveIndexResponseFromES { data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; } -export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; +export type CallAsCurrentUser = IScopedClusterClient['asCurrentUser']; /** * @internal */ export interface SnapshotRestoreContext { - client: ILegacyScopedClusterClient; + client: IScopedClusterClient; } /** @@ -71,8 +66,3 @@ export interface SnapshotRestoreContext { export interface SnapshotRestoreRequestHandlerContext extends RequestHandlerContext { snapshotRestore: SnapshotRestoreContext; } - -/** - * @internal - */ -export type SnapshotRestoreRouter = IRouter; diff --git a/x-pack/plugins/spaces/server/config.test.ts b/x-pack/plugins/spaces/server/config.test.ts index 41c4995b5bcf3..1ce1be0698b1c 100644 --- a/x-pack/plugins/spaces/server/config.test.ts +++ b/x-pack/plugins/spaces/server/config.test.ts @@ -19,7 +19,7 @@ const applyConfigDeprecations = (settings: Record = {}) => { deprecation, path: '', })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index bc53f43c3e8c4..ed541fda6c292 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -24,11 +24,11 @@ export function createConfig$(context: PluginInitializerContext) { return context.config.create>(); } -const disabledDeprecation: ConfigDeprecation = (config, fromPath, log) => { +const disabledDeprecation: ConfigDeprecation = (config, fromPath, addDeprecation) => { if (config.xpack?.spaces?.enabled === false) { - log( - `Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)` - ); + addDeprecation({ + message: `Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)`, + }); } return config; }; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index bc27fdd499368..a0bf944160cee 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -9,7 +9,6 @@ import Boom from '@hapi/boom'; // @ts-ignore import { kibanaTestUser } from '@kbn/test'; -import type { Legacy } from 'kibana'; import type { CoreSetup, IBasePath, IRouter } from 'src/core/server'; import { coreMock, elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import * as kbnTestServer from 'src/core/test_helpers/kbn_server'; @@ -39,69 +38,18 @@ describe.skip('onPostAuthInterceptor', () => { * commented out due to hooks being called regardless of skip * https://github.com/facebook/jest/issues/8379 - beforeEach(async () => { + beforeEach(async () => { root = kbnTestServer.createRoot(); }); - afterEach(async () => await root.shutdown()); + afterEach(async () => await root.shutdown()); - */ + */ - function initKbnServer(router: IRouter, basePath: IBasePath, routes: 'legacy' | 'new-platform') { - const kbnServer = kbnTestServer.getKbnServer(root); - - if (routes === 'legacy') { - kbnServer.server.route([ - { - method: 'GET', - path: '/foo', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/app/kibana', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/app/app-1', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/app/app-2', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/api/test/foo', - handler: (req: Legacy.Request) => { - return { path: req.path, basePath: basePath.get(req) }; - }, - }, - { - method: 'GET', - path: '/some/path/s/foo/bar', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - ]); - } - - if (routes === 'new-platform') { - router.get({ path: '/api/np_test/foo', validate: false }, (context, req, h) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - }); - } + function initKbnServer(router: IRouter, basePath: IBasePath) { + router.get({ path: '/api/np_test/foo', validate: false }, (context, req, h) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + }); } async function request( @@ -205,12 +153,10 @@ describe.skip('onPostAuthInterceptor', () => { const router = http.createRouter('/'); - initKbnServer(router, http.basePath, 'new-platform'); + initKbnServer(router, http.basePath); await root.start(); - initKbnServer(router, http.basePath, 'legacy'); - const response = await kbnTestServer.request.get(root, path); return { @@ -219,58 +165,6 @@ describe.skip('onPostAuthInterceptor', () => { }; } - describe('requests proxied to the legacy platform', () => { - it('redirects to the space selector screen when accessing an app within a non-existent space', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - }, - ]; - - const { response } = await request('/s/not-found/app/kibana', spaces); - - expect(response.status).toEqual(302); - expect(response.header.location).toEqual(`/spaces/space_selector`); - }); - - it('when accessing the kibana app it always allows the request to continue', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - disabledFeatures: ['feature-1', 'feature-2', 'feature-4', 'feature-5'], - }, - }, - ]; - - const { response } = await request('/s/a-space/app/kibana', spaces); - - expect(response.status).toEqual(200); - }); - - it('allows the request to continue when accessing an API endpoint within a non-existent space', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - }, - ]; - - const { response } = await request('/s/not-found/api/test/foo', spaces); - - expect(response.status).toEqual(200); - }); - }); - describe('requests handled completely in the new platform', () => { it('redirects to the space selector screen when accessing an app within a non-existent space', async () => { const spaces = [ diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 27164109de74d..4bb21500f7bfc 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -6,7 +6,6 @@ */ import { schema } from '@kbn/config-schema'; -import type { Legacy } from 'kibana'; import type { CoreSetup, IBasePath, @@ -28,90 +27,57 @@ describe.skip('onRequestInterceptor', () => { * commented out due to hooks being called regardless of skip * https://github.com/facebook/jest/issues/8379 - beforeEach(async () => { + beforeEach(async () => { root = kbnTestServer.createRoot(); }, 30000); - afterEach(async () => await root.shutdown()); + afterEach(async () => await root.shutdown()); - */ + */ - function initKbnServer(router: IRouter, basePath: IBasePath, routes: 'legacy' | 'new-platform') { - const kbnServer = kbnTestServer.getKbnServer(root); + function initKbnServer(router: IRouter, basePath: IBasePath) { + router.get( + { path: '/np_foo', validate: false }, + (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + } + ); - if (routes === 'legacy') { - kbnServer.server.route([ - { - method: 'GET', - path: '/foo', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/some/path/s/foo/bar', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ path: req.path, basePath: basePath.get(req) }); - }, - }, - { - method: 'GET', - path: '/i/love/spaces', - handler: (req: Legacy.Request, h: Legacy.ResponseToolkit) => { - return h.response({ - path: req.path, - basePath: basePath.get(req), - query: req.query, - }); - }, - }, - ]); - } - - if (routes === 'new-platform') { - router.get( - { path: '/np_foo', validate: false }, - (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - } - ); - - router.get( - { path: '/some/path/s/np_foo/bar', validate: false }, - (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { - return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); - } - ); - - router.get( - { - path: '/i/love/np_spaces', - validate: { - query: schema.object({ - queryParam: schema.string({ - defaultValue: 'oh noes, this was not set on the request correctly', - }), + router.get( + { path: '/some/path/s/np_foo/bar', validate: false }, + (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { + return h.ok({ body: { path: req.url.pathname, basePath: basePath.get(req) } }); + } + ); + + router.get( + { + path: '/i/love/np_spaces', + validate: { + query: schema.object({ + queryParam: schema.string({ + defaultValue: 'oh noes, this was not set on the request correctly', }), - }, + }), }, - (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { - return h.ok({ - body: { - path: req.url.pathname, - basePath: basePath.get(req), - query: req.query, - }, - }); - } - ); - } + }, + (context: unknown, req: KibanaRequest, h: KibanaResponseFactory) => { + return h.ok({ + body: { + path: req.url.pathname, + basePath: basePath.get(req), + query: req.query, + }, + }); + } + ); } interface SetupOpts { basePath: string; routes: 'legacy' | 'new-platform'; } + async function setup(opts: SetupOpts = { basePath: '/', routes: 'legacy' }) { const { http, elasticsearch } = await root.setup(); // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check @@ -123,69 +89,15 @@ describe.skip('onRequestInterceptor', () => { const router = http.createRouter('/'); - initKbnServer(router, http.basePath, 'new-platform'); + initKbnServer(router, http.basePath); await root.start(); - initKbnServer(router, http.basePath, 'legacy'); - return { http, }; } - describe('requests proxied to the legacy platform', () => { - it('handles paths without a space identifier', async () => { - await setup(); - - const path = '/foo'; - - await kbnTestServer.request.get(root, path).expect(200, { - path, - basePath: '', // no base path set for route within the default space - }); - }, 30000); - - it('strips the Space URL Context from the request', async () => { - await setup(); - - const path = '/s/foo-space/foo'; - - const resp = await kbnTestServer.request.get(root, path); - - expect(resp.status).toEqual(200); - expect(resp.body).toEqual({ - path: '/foo', - basePath: '/s/foo-space', - }); - }, 30000); - - it('ignores space identifiers in the middle of the path', async () => { - await setup(); - - const path = '/some/path/s/foo/bar'; - - await kbnTestServer.request.get(root, path).expect(200, { - path: '/some/path/s/foo/bar', - basePath: '', // no base path set for route within the default space - }); - }, 30000); - - it('strips the Space URL Context from the request, maintaining the rest of the path', async () => { - await setup(); - - const path = '/s/foo/i/love/spaces?queryParam=queryValue'; - - await kbnTestServer.request.get(root, path).expect(200, { - path: '/i/love/spaces', - basePath: '/s/foo', - query: { - queryParam: 'queryValue', - }, - }); - }, 30000); - }); - describe('requests handled completely in the new platform', () => { it('handles paths without a space identifier', async () => { await setup({ basePath: '/', routes: 'new-platform' }); diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 470b47b40f67d..3fce5f7bdfdf4 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -22,7 +22,7 @@ const applyTaskManagerDeprecations = (settings: Record = {}) => deprecation, path: CONFIG_PATH, })), - (msg) => deprecationMessages.push(msg) + () => ({ message }) => deprecationMessages.push(message) ); return { messages: deprecationMessages, diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 6d744010757f5..a34f5a87fddbe 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -31,17 +31,18 @@ export { export const config: PluginConfigDescriptor = { schema: configSchema, deprecations: () => [ - (settings, fromPath, log) => { + (settings, fromPath, addDeprecation) => { const taskManager = get(settings, fromPath); if (taskManager?.index) { - log( - `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details` - ); + addDeprecation({ + documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', + message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, + }); } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { - log( - `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.` - ); + addDeprecation({ + message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, + }); } return settings; }, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index ef09937da3fbc..e1e0711c2bb2c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1837,17 +1837,35 @@ }, "agents": { "properties": { - "total": { - "type": "long" + "total_enrolled": { + "type": "long", + "_meta": { + "description": "The total number of enrolled agents, in any state" + } }, - "online": { - "type": "long" + "healthy": { + "type": "long", + "_meta": { + "description": "The total number of enrolled agents in a healthy state" + } }, - "error": { - "type": "long" + "unhealthy": { + "type": "long", + "_meta": { + "description": "The total number of enrolled agents in an unhealthy state" + } }, "offline": { - "type": "long" + "type": "long", + "_meta": { + "description": "The total number of enrolled agents currently offline" + } + }, + "total_all_statuses": { + "type": "long", + "_meta": { + "description": "The total number of agents in any state, both enrolled and inactive" + } } } }, @@ -1978,6 +1996,42 @@ "xy_layer_added": { "type": "long" }, + "open_field_editor_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + } + }, + "open_field_editor_add": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to add a field from within Lens." + } + }, + "save_field_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user edited a field from within Lens." + } + }, + "save_field_add": { + "type": "long", + "_meta": { + "description": "Number of times the user added a field from within Lens." + } + }, + "open_field_delete_modal": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the field delete modal from within Lens." + } + }, + "delete_field": { + "type": "long", + "_meta": { + "description": "Number of times the user deleted a field from within Lens." + } + }, "indexpattern_dimension_operation_terms": { "type": "long", "_meta": { @@ -2034,70 +2088,50 @@ }, "indexpattern_dimension_operation_range": { "type": "long", - "_meta": { "description": "Number of times the range function was selected" } + "_meta": { + "description": "Number of times the range function was selected" + } }, "indexpattern_dimension_operation_median": { "type": "long", - "_meta": { "description": "Number of times the median function was selected" } + "_meta": { + "description": "Number of times the median function was selected" + } }, "indexpattern_dimension_operation_percentile": { - "type": "long", - "_meta": { "description": "Number of times the percentile function was selected" } - }, - "indexpattern_dimension_operation_last_value": { - "type": "long", - "_meta": { "description": "Number of times the last value function was selected" } - }, - "indexpattern_dimension_operation_cumulative_sum": { - "type": "long", - "_meta": { "description": "Number of times the cumulative sum function was selected" } - }, - "indexpattern_dimension_operation_counter_rate": { - "type": "long", - "_meta": { "description": "Number of times the counter rate function was selected" } - }, - "indexpattern_dimension_operation_derivative": { - "type": "long", - "_meta": { "description": "Number of times the derivative function was selected" } - }, - "indexpattern_dimension_operation_moving_average": { - "type": "long", - "_meta": { "description": "Number of times the moving average function was selected" } - }, - "open_field_editor_edit": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + "description": "Number of times the percentile function was selected" } }, - "open_field_editor_add": { + "indexpattern_dimension_operation_last_value": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to add a field from within Lens." + "description": "Number of times the last value function was selected" } }, - "save_field_edit": { + "indexpattern_dimension_operation_cumulative_sum": { "type": "long", "_meta": { - "description": "Number of times the user edited a field from within Lens." + "description": "Number of times the cumulative sum function was selected" } }, - "save_field_add": { + "indexpattern_dimension_operation_counter_rate": { "type": "long", "_meta": { - "description": "Number of times the user added a field from within Lens." + "description": "Number of times the counter rate function was selected" } }, - "open_field_delete_modal": { + "indexpattern_dimension_operation_derivative": { "type": "long", "_meta": { - "description": "Number of times the user opened the field delete modal from within Lens." + "description": "Number of times the derivative function was selected" } }, - "delete_field": { + "indexpattern_dimension_operation_moving_average": { "type": "long", "_meta": { - "description": "Number of times the user deleted a field from within Lens." + "description": "Number of times the moving average function was selected" } } } @@ -2185,6 +2219,42 @@ "xy_layer_added": { "type": "long" }, + "open_field_editor_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + } + }, + "open_field_editor_add": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to add a field from within Lens." + } + }, + "save_field_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user edited a field from within Lens." + } + }, + "save_field_add": { + "type": "long", + "_meta": { + "description": "Number of times the user added a field from within Lens." + } + }, + "open_field_delete_modal": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the field delete modal from within Lens." + } + }, + "delete_field": { + "type": "long", + "_meta": { + "description": "Number of times the user deleted a field from within Lens." + } + }, "indexpattern_dimension_operation_terms": { "type": "long", "_meta": { @@ -2241,70 +2311,50 @@ }, "indexpattern_dimension_operation_range": { "type": "long", - "_meta": { "description": "Number of times the range function was selected" } + "_meta": { + "description": "Number of times the range function was selected" + } }, "indexpattern_dimension_operation_median": { "type": "long", - "_meta": { "description": "Number of times the median function was selected" } + "_meta": { + "description": "Number of times the median function was selected" + } }, "indexpattern_dimension_operation_percentile": { - "type": "long", - "_meta": { "description": "Number of times the percentile function was selected" } - }, - "indexpattern_dimension_operation_last_value": { - "type": "long", - "_meta": { "description": "Number of times the last value function was selected" } - }, - "indexpattern_dimension_operation_cumulative_sum": { - "type": "long", - "_meta": { "description": "Number of times the cumulative sum function was selected" } - }, - "indexpattern_dimension_operation_counter_rate": { - "type": "long", - "_meta": { "description": "Number of times the counter rate function was selected" } - }, - "indexpattern_dimension_operation_derivative": { - "type": "long", - "_meta": { "description": "Number of times the derivative function was selected" } - }, - "indexpattern_dimension_operation_moving_average": { - "type": "long", - "_meta": { "description": "Number of times the moving average function was selected" } - }, - "open_field_editor_edit": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + "description": "Number of times the percentile function was selected" } }, - "open_field_editor_add": { + "indexpattern_dimension_operation_last_value": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to add a field from within Lens." + "description": "Number of times the last value function was selected" } }, - "save_field_edit": { + "indexpattern_dimension_operation_cumulative_sum": { "type": "long", "_meta": { - "description": "Number of times the user edited a field from within Lens." + "description": "Number of times the cumulative sum function was selected" } }, - "save_field_add": { + "indexpattern_dimension_operation_counter_rate": { "type": "long", "_meta": { - "description": "Number of times the user added a field from within Lens." + "description": "Number of times the counter rate function was selected" } }, - "open_field_delete_modal": { + "indexpattern_dimension_operation_derivative": { "type": "long", "_meta": { - "description": "Number of times the user opened the field delete modal from within Lens." + "description": "Number of times the derivative function was selected" } }, - "delete_field": { + "indexpattern_dimension_operation_moving_average": { "type": "long", "_meta": { - "description": "Number of times the user deleted a field from within Lens." + "description": "Number of times the moving average function was selected" } } } diff --git a/x-pack/plugins/timelines/.eslintrc.js b/x-pack/plugins/timelines/.eslintrc.js new file mode 100644 index 0000000000000..b267018448ba6 --- /dev/null +++ b/x-pack/plugins/timelines/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@kbn/eslint/require-license-header': 'off', + }, +}; diff --git a/x-pack/plugins/timelines/.i18nrc.json b/x-pack/plugins/timelines/.i18nrc.json new file mode 100644 index 0000000000000..4fe01ccc7bc69 --- /dev/null +++ b/x-pack/plugins/timelines/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "timelines", + "paths": { + "timelines": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/x-pack/plugins/timelines/README.md b/x-pack/plugins/timelines/README.md new file mode 100644 index 0000000000000..441a505903698 --- /dev/null +++ b/x-pack/plugins/timelines/README.md @@ -0,0 +1,11 @@ +# timelines +Timelines is a plugin that provides a grid component with accompanying server side apis to help users identify events of interest and perform root cause analysis within Kibana. + + +## Using timelines in another plugin +- Add `TimelinesPluginSetup` to Kibana plugin `SetupServices` dependencies: + +```ts +timelines: TimelinesPluginSetup; +``` +- Once `timelines` is added as a required plugin in the consuming plugin's kibana.json, timeline functionality will be available as any other kibana plugin, ie PluginSetupDependencies.timelines.getTimeline() diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts new file mode 100644 index 0000000000000..2354c513f73b8 --- /dev/null +++ b/x-pack/plugins/timelines/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'timelines'; +export const PLUGIN_NAME = 'timelines'; diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json new file mode 100644 index 0000000000000..552ddfd25ce73 --- /dev/null +++ b/x-pack/plugins/timelines/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "timelines", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "timelines"], + "server": true, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx new file mode 100644 index 0000000000000..3388b3c44baff --- /dev/null +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +import { PLUGIN_NAME } from '../../common'; +import { TimelineProps } from '../types'; + +export const Timeline = (props: TimelineProps) => { + return ( + +
+ +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { Timeline as default }; diff --git a/x-pack/plugins/timelines/public/index.scss b/x-pack/plugins/timelines/public/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts new file mode 100644 index 0000000000000..b535def809de3 --- /dev/null +++ b/x-pack/plugins/timelines/public/index.ts @@ -0,0 +1,11 @@ +import './index.scss'; + +import { PluginInitializerContext } from 'src/core/public'; +import { TimelinesPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin(initializerContext: PluginInitializerContext) { + return new TimelinesPlugin(initializerContext); +} +export { TimelinesPluginSetup } from './types'; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx new file mode 100644 index 0000000000000..f999e14ce910c --- /dev/null +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { TimelineProps } from '../types'; + +export const getTimelineLazy = (props: TimelineProps) => { + const TimelineLazy = lazy(() => import('../components')); + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts new file mode 100644 index 0000000000000..7e90d9467fefd --- /dev/null +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -0,0 +1,24 @@ +import { CoreSetup, Plugin, PluginInitializerContext } from '../../../../src/core/public'; +import { TimelinesPluginSetup, TimelineProps } from './types'; +import { getTimelineLazy } from './methods'; + +export class TimelinesPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup): TimelinesPluginSetup { + const config = this.initializerContext.config.get<{ enabled: boolean }>(); + if (!config.enabled) { + return {}; + } + + return { + getTimeline: (props: TimelineProps) => { + return getTimelineLazy(props); + }, + }; + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts new file mode 100644 index 0000000000000..b199b45902718 --- /dev/null +++ b/x-pack/plugins/timelines/public/types.ts @@ -0,0 +1,9 @@ +import { ReactElement } from 'react'; + +export interface TimelinesPluginSetup { + getTimeline?: (props: TimelineProps) => ReactElement; +} + +export interface TimelineProps { + timelineId: string; +} diff --git a/x-pack/plugins/timelines/server/config.ts b/x-pack/plugins/timelines/server/config.ts new file mode 100644 index 0000000000000..633a95b8f91a7 --- /dev/null +++ b/x-pack/plugins/timelines/server/config.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/timelines/server/index.ts b/x-pack/plugins/timelines/server/index.ts new file mode 100644 index 0000000000000..32de97be2704a --- /dev/null +++ b/x-pack/plugins/timelines/server/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { TimelinesPlugin } from './plugin'; +import { ConfigSchema } from './config'; + +export const config = { + schema: ConfigSchema, + exposeToBrowser: { + enabled: true, + }, +}; +export function plugin(initializerContext: PluginInitializerContext) { + return new TimelinesPlugin(initializerContext); +} + +export { TimelinesPluginSetup, TimelinesPluginStart } from './types'; diff --git a/x-pack/plugins/timelines/server/plugin.ts b/x-pack/plugins/timelines/server/plugin.ts new file mode 100644 index 0000000000000..3e330b19b7fdb --- /dev/null +++ b/x-pack/plugins/timelines/server/plugin.ts @@ -0,0 +1,35 @@ +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../../src/core/server'; + +import { TimelinesPluginSetup, TimelinesPluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class TimelinesPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('timelines: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('timelines: Started'); + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/timelines/server/routes/index.ts b/x-pack/plugins/timelines/server/routes/index.ts new file mode 100644 index 0000000000000..edb10c579b30b --- /dev/null +++ b/x-pack/plugins/timelines/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '../../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/timeline/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/x-pack/plugins/timelines/server/types.ts b/x-pack/plugins/timelines/server/types.ts new file mode 100644 index 0000000000000..cb544562b79b4 --- /dev/null +++ b/x-pack/plugins/timelines/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TimelinesPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TimelinesPluginStart {} diff --git a/x-pack/plugins/maps_legacy_licensing/tsconfig.json b/x-pack/plugins/timelines/tsconfig.json similarity index 56% rename from x-pack/plugins/maps_legacy_licensing/tsconfig.json rename to x-pack/plugins/timelines/tsconfig.json index 30a547b18a831..67e606e798c03 100644 --- a/x-pack/plugins/maps_legacy_licensing/tsconfig.json +++ b/x-pack/plugins/timelines/tsconfig.json @@ -7,9 +7,13 @@ "declaration": true, "declarationMap": true }, - "include": ["public/**/*"], + "include": [ + // add all the folders contains files to be compiled + "common/**/*", + "public/**/*", + "server/**/*" + ], "references": [ - { "path": "../licensing/tsconfig.json" }, - { "path": "../../../src/plugins/maps_ems/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" }, ] } diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts index 476e2bad853c9..4b66de9be20d2 100644 --- a/x-pack/plugins/transform/common/api_schemas/type_guards.ts +++ b/x-pack/plugins/transform/common/api_schemas/type_guards.ts @@ -5,10 +5,10 @@ * 2.0. */ -import type { SearchResponse7 } from '../../../ml/common'; +import type { estypes } from '@elastic/elasticsearch'; import type { EsIndex } from '../types/es_index'; -import { isPopulatedObject } from '../utils/object_utils'; +import { isPopulatedObject } from '../shared_imports'; // To be able to use the type guards on the client side, we need to make sure we don't import // the code of '@kbn/config-schema' but just its types, otherwise the client side code will @@ -28,20 +28,13 @@ import type { GetTransformsStatsResponseSchema } from './transforms_stats'; import type { PostTransformsUpdateResponseSchema } from './update_transforms'; const isGenericResponseSchema = (arg: any): arg is T => { - return ( - isPopulatedObject(arg) && - {}.hasOwnProperty.call(arg, 'count') && - {}.hasOwnProperty.call(arg, 'transforms') && - Array.isArray(arg.transforms) - ); + return isPopulatedObject(arg, ['count', 'transforms']) && Array.isArray(arg.transforms); }; export const isGetTransformNodesResponseSchema = ( arg: unknown ): arg is GetTransformNodesResponseSchema => { - return ( - isPopulatedObject(arg) && {}.hasOwnProperty.call(arg, 'count') && typeof arg.count === 'number' - ); + return isPopulatedObject(arg, ['count']) && typeof arg.count === 'number'; }; export const isGetTransformsResponseSchema = (arg: unknown): arg is GetTransformsResponseSchema => { @@ -59,7 +52,7 @@ export const isDeleteTransformsResponseSchema = ( ): arg is DeleteTransformsResponseSchema => { return ( isPopulatedObject(arg) && - Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'transformDeleted'))) + Object.values(arg).every((d) => isPopulatedObject(d, ['transformDeleted'])) ); }; @@ -67,8 +60,22 @@ export const isEsIndices = (arg: unknown): arg is EsIndex[] => { return Array.isArray(arg); }; -export const isEsSearchResponse = (arg: unknown): arg is SearchResponse7 => { - return isPopulatedObject(arg) && {}.hasOwnProperty.call(arg, 'hits'); +export const isEsSearchResponse = (arg: unknown): arg is estypes.SearchResponse => { + return isPopulatedObject(arg, ['hits']); +}; + +type SearchResponseWithAggregations = Required> & + estypes.SearchResponse; +export const isEsSearchResponseWithAggregations = ( + arg: unknown +): arg is SearchResponseWithAggregations => { + return isEsSearchResponse(arg) && {}.hasOwnProperty.call(arg, 'aggregations'); +}; + +export const isMultiBucketAggregate = ( + arg: unknown +): arg is estypes.MultiBucketAggregate => { + return isPopulatedObject(arg, ['buckets']); }; export const isFieldHistogramsResponseSchema = ( @@ -87,9 +94,7 @@ export const isPostTransformsPreviewResponseSchema = ( arg: unknown ): arg is PostTransformsPreviewResponseSchema => { return ( - isPopulatedObject(arg) && - {}.hasOwnProperty.call(arg, 'generated_dest_index') && - {}.hasOwnProperty.call(arg, 'preview') && + isPopulatedObject(arg, ['generated_dest_index', 'preview']) && typeof arg.generated_dest_index !== undefined && Array.isArray(arg.preview) ); @@ -98,21 +103,19 @@ export const isPostTransformsPreviewResponseSchema = ( export const isPostTransformsUpdateResponseSchema = ( arg: unknown ): arg is PostTransformsUpdateResponseSchema => { - return isPopulatedObject(arg) && {}.hasOwnProperty.call(arg, 'id') && typeof arg.id === 'string'; + return isPopulatedObject(arg, ['id']) && typeof arg.id === 'string'; }; export const isPutTransformsResponseSchema = (arg: unknown): arg is PutTransformsResponseSchema => { return ( - isPopulatedObject(arg) && - {}.hasOwnProperty.call(arg, 'transformsCreated') && - {}.hasOwnProperty.call(arg, 'errors') && + isPopulatedObject(arg, ['transformsCreated', 'errors']) && Array.isArray(arg.transformsCreated) && Array.isArray(arg.errors) ); }; const isGenericSuccessResponseSchema = (arg: unknown) => - isPopulatedObject(arg) && Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'success'))); + isPopulatedObject(arg) && Object.values(arg).every((d) => isPopulatedObject(d, ['success'])); export const isStartTransformsResponseSchema = ( arg: unknown diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts index 3062c7ab8d23c..38cfb6bc457f1 100644 --- a/x-pack/plugins/transform/common/shared_imports.ts +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -5,10 +5,10 @@ * 2.0. */ -export type { HitsTotalRelation, SearchResponse7 } from '../../ml/common'; export { composeValidators, + isPopulatedObject, + isRuntimeMappings, patternValidator, ChartData, - HITS_TOTAL_RELATION, } from '../../ml/common'; diff --git a/x-pack/plugins/transform/common/types/index_pattern.ts b/x-pack/plugins/transform/common/types/index_pattern.ts index bab31b67b2b61..0485de8982e1a 100644 --- a/x-pack/plugins/transform/common/types/index_pattern.ts +++ b/x-pack/plugins/transform/common/types/index_pattern.ts @@ -7,17 +7,17 @@ import type { IndexPattern } from '../../../../../src/plugins/data/common'; -import { isPopulatedObject } from '../utils/object_utils'; +import { isPopulatedObject } from '../shared_imports'; // Custom minimal type guard for IndexPattern to check against the attributes used in transforms code. export function isIndexPattern(arg: any): arg is IndexPattern { return ( - isPopulatedObject(arg) && - 'getComputedFields' in arg && - typeof arg.getComputedFields === 'function' && - {}.hasOwnProperty.call(arg, 'title') && + isPopulatedObject(arg, ['title', 'fields']) && + // `getComputedFields` is inherited, so it's not possible to + // check with `hasOwnProperty` which is used by isPopulatedObject() + 'getComputedFields' in (arg as IndexPattern) && + typeof (arg as IndexPattern).getComputedFields === 'function' && typeof arg.title === 'string' && - {}.hasOwnProperty.call(arg, 'fields') && Array.isArray(arg.fields) ); } diff --git a/x-pack/plugins/transform/common/types/transform.ts b/x-pack/plugins/transform/common/types/transform.ts index 600808c475fd1..f1e7efdadca9d 100644 --- a/x-pack/plugins/transform/common/types/transform.ts +++ b/x-pack/plugins/transform/common/types/transform.ts @@ -7,7 +7,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; import type { LatestFunctionConfig, PutTransformsRequestSchema } from '../api_schemas/transforms'; -import { isPopulatedObject } from '../utils/object_utils'; +import { isPopulatedObject } from '../shared_imports'; import { PivotGroupByDict } from './pivot_group_by'; import { PivotAggDict } from './pivot_aggs'; @@ -46,11 +46,11 @@ export type TransformLatestConfig = Omit & { export type TransformConfigUnion = TransformPivotConfig | TransformLatestConfig; export function isPivotTransform(transform: unknown): transform is TransformPivotConfig { - return isPopulatedObject(transform) && transform.hasOwnProperty('pivot'); + return isPopulatedObject(transform, ['pivot']); } export function isLatestTransform(transform: unknown): transform is TransformLatestConfig { - return isPopulatedObject(transform) && transform.hasOwnProperty('latest'); + return isPopulatedObject(transform, ['latest']); } export interface LatestFunctionConfigUI { diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts index 03e6b2e403b69..00ffa40b84d3b 100644 --- a/x-pack/plugins/transform/common/types/transform_stats.ts +++ b/x-pack/plugins/transform/common/types/transform_stats.ts @@ -6,7 +6,7 @@ */ import { TransformState, TRANSFORM_STATE } from '../constants'; -import { isPopulatedObject } from '../utils/object_utils'; +import { isPopulatedObject } from '../shared_imports'; import { TransformId } from './transform'; export interface TransformStats { @@ -61,7 +61,5 @@ function isTransformState(arg: unknown): arg is TransformState { } export function isTransformStats(arg: unknown): arg is TransformStats { - return ( - isPopulatedObject(arg) && {}.hasOwnProperty.call(arg, 'state') && isTransformState(arg.state) - ); + return isPopulatedObject(arg, ['state']) && isTransformState(arg.state); } diff --git a/x-pack/plugins/transform/common/utils/errors.ts b/x-pack/plugins/transform/common/utils/errors.ts index 46ff3f9165c00..2aff8f332b130 100644 --- a/x-pack/plugins/transform/common/utils/errors.ts +++ b/x-pack/plugins/transform/common/utils/errors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPopulatedObject } from './object_utils'; +import { isPopulatedObject } from '../shared_imports'; export interface ErrorResponse { body: { @@ -18,7 +18,11 @@ export interface ErrorResponse { } export function isErrorResponse(arg: unknown): arg is ErrorResponse { - return isPopulatedObject(arg) && isPopulatedObject(arg.body) && arg?.body?.message !== undefined; + return ( + isPopulatedObject(arg, ['body']) && + isPopulatedObject(arg.body, ['message']) && + arg.body.message !== undefined + ); } export function getErrorMessage(error: unknown) { @@ -26,7 +30,7 @@ export function getErrorMessage(error: unknown) { return `${error.body.error}: ${error.body.message}`; } - if (isPopulatedObject(error) && typeof error.message === 'string') { + if (isPopulatedObject(error, ['message']) && typeof error.message === 'string') { return error.message; } diff --git a/x-pack/plugins/transform/common/utils/object_utils.test.ts b/x-pack/plugins/transform/common/utils/object_utils.test.ts index 5b354b9b27589..c99adf6b6d189 100644 --- a/x-pack/plugins/transform/common/utils/object_utils.test.ts +++ b/x-pack/plugins/transform/common/utils/object_utils.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getNestedProperty, isPopulatedObject } from './object_utils'; +import { getNestedProperty } from './object_utils'; describe('object_utils', () => { test('getNestedProperty()', () => { @@ -68,12 +68,4 @@ describe('object_utils', () => { expect(typeof test11).toBe('number'); expect(test11).toBe(0); }); - - test('isPopulatedObject()', () => { - expect(isPopulatedObject(0)).toBe(false); - expect(isPopulatedObject('')).toBe(false); - expect(isPopulatedObject(null)).toBe(false); - expect(isPopulatedObject({})).toBe(false); - expect(isPopulatedObject({ attribute: 'value' })).toBe(true); - }); }); diff --git a/x-pack/plugins/transform/common/utils/object_utils.ts b/x-pack/plugins/transform/common/utils/object_utils.ts index a573535da6b4f..605af48914360 100644 --- a/x-pack/plugins/transform/common/utils/object_utils.ts +++ b/x-pack/plugins/transform/common/utils/object_utils.ts @@ -51,7 +51,3 @@ export const setNestedProperty = (obj: Record, accessor: string, va return obj; }; - -export const isPopulatedObject = >(arg: unknown): arg is T => { - return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0; -}; diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 00a92865789ff..ae072e6666e4a 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -16,4 +16,4 @@ export const useRequest = jest.fn(() => ({ export const createSavedSearchesLoader = jest.fn(); // just passing through the reimports -export { getMlSharedImports, HITS_TOTAL_RELATION } from '../../../ml/public'; +export { getMlSharedImports, ES_CLIENT_TOTAL_HITS_RELATION } from '../../../ml/public'; diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index 905b40f16f7fb..03e06d36f9319 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -14,7 +14,7 @@ import type { Dictionary } from '../../../common/types/common'; import type { EsFieldName } from '../../../common/types/fields'; import type { PivotAgg, PivotSupportedAggs } from '../../../common/types/pivot_aggs'; import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../common/shared_imports'; import { getAggFormConfig } from '../sections/create_transform/components/step_define/common/get_agg_form_config'; import { PivotAggsConfigFilter } from '../sections/create_transform/components/step_define/common/filter_agg/types'; @@ -166,11 +166,7 @@ export type PivotAggsConfigWithUiSupport = export function isPivotAggsConfigWithUiSupport(arg: unknown): arg is PivotAggsConfigWithUiSupport { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('agg') && - arg.hasOwnProperty('aggName') && - arg.hasOwnProperty('dropDownName') && - arg.hasOwnProperty('field') && + isPopulatedObject(arg, ['agg', 'aggName', 'dropDownName', 'field']) && isPivotSupportedAggs(arg.agg) ); } @@ -181,15 +177,12 @@ export function isPivotAggsConfigWithUiSupport(arg: unknown): arg is PivotAggsCo type PivotAggsConfigWithExtendedForm = PivotAggsConfigFilter; export function isPivotAggsWithExtendedForm(arg: unknown): arg is PivotAggsConfigWithExtendedForm { - return isPopulatedObject(arg) && arg.hasOwnProperty('AggFormComponent'); + return isPopulatedObject(arg, ['AggFormComponent']); } export function isPivotAggsConfigPercentiles(arg: unknown): arg is PivotAggsConfigPercentiles { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('agg') && - arg.hasOwnProperty('field') && - arg.hasOwnProperty('percents') && + isPopulatedObject(arg, ['agg', 'field', 'percents']) && arg.agg === PIVOT_SUPPORTED_AGGS.PERCENTILES ); } diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index fac0d88a84df7..0ad059fd29950 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -9,7 +9,7 @@ import { AggName } from '../../../common/types/aggregations'; import { Dictionary } from '../../../common/types/common'; import { EsFieldName } from '../../../common/types/fields'; import { GenericAgg } from '../../../common/types/pivot_group_by'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../common/shared_imports'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; import { PivotAggsConfigWithUiSupport } from './pivot_aggs'; @@ -84,30 +84,21 @@ export type PivotGroupByConfigDict = Dictionary; export function isGroupByDateHistogram(arg: unknown): arg is GroupByDateHistogram { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('agg') && - arg.hasOwnProperty('field') && - arg.hasOwnProperty('calendar_interval') && + isPopulatedObject(arg, ['agg', 'field', 'calendar_interval']) && arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM ); } export function isGroupByHistogram(arg: unknown): arg is GroupByHistogram { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('agg') && - arg.hasOwnProperty('field') && - arg.hasOwnProperty('interval') && + isPopulatedObject(arg, ['agg', 'field', 'interval']) && arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM ); } export function isGroupByTerms(arg: unknown): arg is GroupByTerms { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('agg') && - arg.hasOwnProperty('field') && - arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS + isPopulatedObject(arg, ['agg', 'field']) && arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS ); } @@ -124,5 +115,5 @@ export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): Gen } export function isPivotAggConfigWithUiSupport(arg: unknown): arg is PivotAggsConfigWithUiSupport { - return isPopulatedObject(arg) && arg.hasOwnProperty('agg') && arg.hasOwnProperty('field'); + return isPopulatedObject(arg, ['agg', 'field']); } diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 647d511cc4dcf..a7a3a91f9429b 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { DefaultOperator } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { HttpFetchError } from '../../../../../../src/core/public'; import type { IndexPattern } from '../../../../../../src/plugins/data/public'; @@ -17,7 +17,7 @@ import type { PutTransformsPivotRequestSchema, PutTransformsRequestSchema, } from '../../../common/api_schemas/transforms'; -import { isPopulatedObject } from '../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../common/shared_imports'; import { DateHistogramAgg, HistogramAgg, TermsAgg } from '../../../common/types/pivot_group_by'; import { isIndexPattern } from '../../../common/types/index_pattern'; @@ -39,7 +39,7 @@ import { export interface SimpleQuery { query_string: { query: string; - default_operator?: DefaultOperator; + default_operator?: estypes.DefaultOperator; }; } @@ -59,14 +59,13 @@ export function getPivotQuery(search: string | SavedSearchQuery): PivotQuery { } export function isSimpleQuery(arg: unknown): arg is SimpleQuery { - return isPopulatedObject(arg) && arg.hasOwnProperty('query_string'); + return isPopulatedObject(arg, ['query_string']); } export const matchAllQuery = { match_all: {} }; export function isMatchAllQuery(query: unknown): boolean { return ( - isPopulatedObject(query) && - query.hasOwnProperty('match_all') && + isPopulatedObject(query, ['match_all']) && typeof query.match_all === 'object' && query.match_all !== null && Object.keys(query.match_all).length === 0 @@ -101,7 +100,7 @@ export function getCombinedRuntimeMappings( combinedRuntimeMappings = { ...combinedRuntimeMappings, ...runtimeMappings }; } - if (isPopulatedObject(combinedRuntimeMappings)) { + if (isPopulatedObject(combinedRuntimeMappings)) { return combinedRuntimeMappings; } return undefined; diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index 7aaca793c2a1f..a9455877be429 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; + import { HttpFetchError } from 'kibana/public'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; @@ -37,7 +39,6 @@ import type { PostTransformsUpdateResponseSchema, } from '../../../../common/api_schemas/update_transforms'; -import type { SearchResponse7 } from '../../../../common/shared_imports'; import { EsIndex } from '../../../../common/types/es_index'; import type { SavedSearchQuery } from '../use_search_items'; @@ -134,7 +135,7 @@ const apiFactory = () => ({ ): Promise { return Promise.resolve([]); }, - async esSearch(payload: any): Promise { + async esSearch(payload: any): Promise { return Promise.resolve({ hits: { hits: [], diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index f3c90a688453d..1abe2ed09444e 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -7,6 +7,8 @@ import { useMemo } from 'react'; +import { estypes } from '@elastic/elasticsearch'; + import { HttpFetchError } from 'kibana/public'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; @@ -44,7 +46,6 @@ import type { GetTransformsStatsResponseSchema } from '../../../common/api_schem import { TransformId } from '../../../common/types/transform'; import { API_BASE_PATH } from '../../../common/constants'; import { EsIndex } from '../../../common/types/es_index'; -import type { SearchResponse7 } from '../../../common/shared_imports'; import { useAppDependencies } from '../app_dependencies'; @@ -187,7 +188,7 @@ export const useApi = () => { return e; } }, - async esSearch(payload: any): Promise { + async esSearch(payload: any): Promise { try { return await http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) }); } catch (e) { diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index e12aa78e33622..bb83de8e12004 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -7,7 +7,8 @@ import { useEffect, useMemo } from 'react'; -import { EuiDataGridColumn } from '@elastic/eui'; +import type { estypes } from '@elastic/elasticsearch'; +import type { EuiDataGridColumn } from '@elastic/eui'; import { isEsSearchResponse, @@ -133,10 +134,14 @@ export const useIndexData = ( return; } - const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields)); + const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); - setRowCount(resp.hits.total.value); - setRowCountRelation(resp.hits.total.relation); + setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value); + setRowCountRelation( + typeof resp.hits.total === 'number' + ? ('eq' as estypes.TotalHitsRelation) + : resp.hits.total.relation + ); setTableItems(docs); setStatus(INDEX_STATUS.LOADED); }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index 2477c005c936d..24c28effd12bc 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -18,7 +18,11 @@ import type { PreviewMappingsProperties } from '../../../common/api_schemas/tran import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards'; import { getNestedProperty } from '../../../common/utils/object_utils'; -import { RenderCellValue, UseIndexDataReturnType, HITS_TOTAL_RELATION } from '../../shared_imports'; +import { + RenderCellValue, + UseIndexDataReturnType, + ES_CLIENT_TOTAL_HITS_RELATION, +} from '../../shared_imports'; import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies } from '../app_dependencies'; @@ -128,7 +132,7 @@ export const usePivotData = ( if (!validationStatus.isValid) { setTableItems([]); setRowCount(0); - setRowCountRelation(HITS_TOTAL_RELATION.EQ); + setRowCountRelation(ES_CLIENT_TOTAL_HITS_RELATION.EQ); setNoDataMessage(validationStatus.errorMessage!); return; } @@ -149,7 +153,7 @@ export const usePivotData = ( setErrorMessage(getErrorMessage(resp)); setTableItems([]); setRowCount(0); - setRowCountRelation(HITS_TOTAL_RELATION.EQ); + setRowCountRelation(ES_CLIENT_TOTAL_HITS_RELATION.EQ); setPreviewMappingsProperties({}); setStatus(INDEX_STATUS.ERROR); return; @@ -157,7 +161,7 @@ export const usePivotData = ( setTableItems(resp.preview); setRowCount(resp.preview.length); - setRowCountRelation(HITS_TOTAL_RELATION.EQ); + setRowCountRelation(ES_CLIENT_TOTAL_HITS_RELATION.EQ); setPreviewMappingsProperties(resp.generated_dest_index.mappings.properties); setStatus(INDEX_STATUS.LOADED); diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index 28e9f190a9108..5599e3f423277 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Privileges } from '../../../../../common/types/privileges'; -import { isPopulatedObject } from '../../../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../../../common/shared_imports'; export interface Capabilities { canGetTransform: boolean; @@ -22,10 +22,8 @@ export type Privilege = [string, string]; function isPrivileges(arg: unknown): arg is Privileges { return ( - isPopulatedObject(arg) && - arg.hasOwnProperty('hasAllPrivileges') && + isPopulatedObject(arg, ['hasAllPrivileges', 'missingPrivileges']) && typeof arg.hasAllPrivileges === 'boolean' && - arg.hasOwnProperty('missingPrivileges') && typeof arg.missingPrivileges === 'object' && arg.missingPrivileges !== null ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx index 1e8397a4d9cc3..1e6e6a971a81a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx @@ -12,8 +12,9 @@ import { EuiCodeEditor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { isRuntimeMappings } from '../../../../../../common/shared_imports'; + import { StepDefineFormHook } from '../step_define'; -import { isRuntimeMappings } from '../step_define/common/types'; export const AdvancedRuntimeMappingsEditor: FC = memo( ({ diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index a7f2a3cd7178d..36bdca7921622 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -47,7 +47,7 @@ import { PutTransformsPivotRequestSchema, } from '../../../../../../common/api_schemas/transforms'; import type { RuntimeField } from '../../../../../../../../../src/plugins/data/common/index_patterns'; -import { isPopulatedObject } from '../../../../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../../../../common/shared_imports'; import { isLatestTransform } from '../../../../../../common/types/transform'; export interface StepDetailsExposedState { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx index 3b5d6e0e50497..9b349541a78a3 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx @@ -16,7 +16,7 @@ import { getFilterAggTypeConfig } from '../config'; import type { FilterAggType, PivotAggsConfigFilter } from '../types'; import type { RuntimeMappings } from '../../types'; import { getKibanaFieldTypeFromEsType } from '../../get_pivot_dropdown_options'; -import { isPopulatedObject } from '../../../../../../../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../../../../../../../common/shared_imports'; /** * Resolves supported filters for provided field. diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index f2db6167c163c..358bb9dcafa96 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -6,12 +6,16 @@ */ import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { estypes } from '@elastic/elasticsearch'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { i18n } from '@kbn/i18n'; -import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards'; +import { + isEsSearchResponseWithAggregations, + isMultiBucketAggregate, +} from '../../../../../../../../../common/api_schemas/type_guards'; import { useApi } from '../../../../../../../hooks'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; import { FilterAggConfigTerm } from '../types'; @@ -29,7 +33,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm const { indexPattern, runtimeMappings } = useContext(CreateTransformWizardContext); const toastNotifications = useToastNotifications(); - const [options, setOptions] = useState([]); + const [options, setOptions] = useState([]); const [isLoading, setIsLoading] = useState(true); /* eslint-disable-next-line react-hooks/exhaustive-deps */ @@ -62,7 +66,12 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm setIsLoading(false); - if (!isEsSearchResponse(response)) { + if ( + !( + isEsSearchResponseWithAggregations(response) && + isMultiBucketAggregate(response.aggregations.field_values) + ) + ) { toastNotifications.addWarning( i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', { defaultMessage: 'Unable to fetch suggestions', @@ -72,9 +81,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm } setOptions( - response.aggregations.field_values.buckets.map( - (value: { key: string; doc_count: number }) => ({ label: value.key }) - ) + response.aggregations.field_values.buckets.map((value) => ({ label: value.key + '' })) ); }, 600), [selectedField] diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 8d85988424e27..957439810adc7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -13,6 +13,7 @@ import { } from '../../../../../../../../../../src/plugins/data/public'; import { getNestedProperty } from '../../../../../../../common/utils/object_utils'; +import { isRuntimeMappings } from '../../../../../../../common/shared_imports'; import { DropDownLabel, @@ -26,7 +27,6 @@ import { import { getDefaultAggregationConfig } from './get_default_aggregation_config'; import { getDefaultGroupByConfig } from './get_default_group_by_config'; import type { Field, StepDefineExposedState } from './types'; -import { isRuntimeMappings } from './types'; const illegalEsAggNameChars = /[[\]>]/g; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts deleted file mode 100644 index ec90d31a0d169..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isRuntimeField, isRuntimeMappings } from './types'; - -describe('Transform: step_define type guards', () => { - it('isRuntimeField()', () => { - expect(isRuntimeField(1)).toBe(false); - expect(isRuntimeField(null)).toBe(false); - expect(isRuntimeField([])).toBe(false); - expect(isRuntimeField({})).toBe(false); - expect(isRuntimeField({ someAttribute: 'someValue' })).toBe(false); - expect(isRuntimeField({ type: 'wrong-type' })).toBe(false); - expect(isRuntimeField({ type: 'keyword', someAttribute: 'some value' })).toBe(false); - - expect(isRuntimeField({ type: 'keyword' })).toBe(true); - expect(isRuntimeField({ type: 'keyword', script: 'some script' })).toBe(true); - }); - - it('isRuntimeMappings()', () => { - expect(isRuntimeMappings(1)).toBe(false); - expect(isRuntimeMappings(null)).toBe(false); - expect(isRuntimeMappings([])).toBe(false); - expect(isRuntimeMappings({})).toBe(false); - expect(isRuntimeMappings({ someAttribute: 'someValue' })).toBe(false); - expect(isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: 'someValue' })).toBe( - false - ); - expect( - isRuntimeMappings({ - fieldName1: { type: 'keyword' }, - fieldName2: { type: 'keyword', someAttribute: 'some value' }, - }) - ).toBe(false); - expect( - isRuntimeMappings({ - fieldName: { type: 'long', script: 1234 }, - }) - ).toBe(false); - expect( - isRuntimeMappings({ - fieldName: { type: 'long', script: { someAttribute: 'some value' } }, - }) - ).toBe(false); - expect( - isRuntimeMappings({ - fieldName: { type: 'long', script: { source: 1234 } }, - }) - ).toBe(false); - - expect(isRuntimeMappings({ fieldName: { type: 'keyword' } })).toBe(true); - expect( - isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: { type: 'keyword' } }) - ).toBe(true); - expect( - isRuntimeMappings({ - fieldName1: { type: 'keyword' }, - fieldName2: { type: 'keyword', script: 'some script as script' }, - }) - ).toBe(true); - expect( - isRuntimeMappings({ - fieldName: { type: 'long', script: { source: 'some script as source' } }, - }) - ).toBe(true); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 6b4ff0090a497..8b3b33fdde3ef 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -24,7 +24,7 @@ import { } from '../../../../../../../common/types/transform'; import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; -import { isPopulatedObject } from '../../../../../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../../../../../common/shared_imports'; export interface ErrorMessage { query: string; @@ -72,30 +72,10 @@ export interface StepDefineExposedState { isRuntimeMappingsEditorEnabled: boolean; } -export function isRuntimeField(arg: unknown): arg is RuntimeField { - return ( - isPopulatedObject(arg) && - ((Object.keys(arg).length === 1 && arg.hasOwnProperty('type')) || - (Object.keys(arg).length === 2 && - arg.hasOwnProperty('type') && - arg.hasOwnProperty('script') && - (typeof arg.script === 'string' || - (isPopulatedObject(arg.script) && - Object.keys(arg.script).length === 1 && - arg.script.hasOwnProperty('source') && - typeof arg.script.source === 'string')))) && - RUNTIME_FIELD_TYPES.includes(arg.type as RuntimeType) - ); -} - -export function isRuntimeMappings(arg: unknown): arg is RuntimeMappings { - return isPopulatedObject(arg) && Object.values(arg).every((d) => isRuntimeField(d)); -} - export function isPivotPartialRequest(arg: unknown): arg is { pivot: PivotConfigDefinition } { - return isPopulatedObject(arg) && arg.hasOwnProperty('pivot'); + return isPopulatedObject(arg, ['pivot']); } export function isLatestPartialRequest(arg: unknown): arg is { latest: LatestFunctionConfig } { - return isPopulatedObject(arg) && arg.hasOwnProperty('latest'); + return isPopulatedObject(arg, ['latest']); } diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index ddf5cf7cb5cb1..edd27fd43c2af 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -15,7 +15,7 @@ export { UseIndexDataReturnType, EsSorting, RenderCellValue, - HITS_TOTAL_RELATION, + ES_CLIENT_TOTAL_HITS_RELATION, } from '../../ml/public'; import { XJson } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts index afdcc93998303..c9a0795c32210 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isPopulatedObject } from '../../../common/utils/object_utils'; +import { isPopulatedObject } from '../../../common/shared_imports'; import { RouteDependencies } from '../../types'; @@ -24,10 +24,7 @@ export const isNodes = (arg: unknown): arg is Nodes => { return ( isPopulatedObject(arg) && Object.values(arg).every( - (node) => - isPopulatedObject(node) && - {}.hasOwnProperty.call(node, NODE_ROLES) && - Array.isArray(node.roles) + (node) => isPopulatedObject(node, [NODE_ROLES]) && Array.isArray(node.roles) ) ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7f257d37cd36d..dc038c1a7959d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2097,7 +2097,7 @@ "home.tutorials.common.auditbeatStatusCheck.successText": "データを受信しました", "home.tutorials.common.auditbeatStatusCheck.text": "Auditbeat からデータを受け取ったことを確認してください。", "home.tutorials.common.auditbeatStatusCheck.title": "ステータス", - "home.tutorials.common.cloudInstructions.passwordAndResetLink": "{passwordTemplate}が「Elastic」ユーザーのパスワードです。\\{#config.cloud.resetPasswordUrl\\}\n パスワードを忘れた場合[Elastic Cloudでリセット] (\\{config.cloud.resetPasswordUrl\\}) 。\n \\{/config.cloud.resetPasswordUrl\\}", + "home.tutorials.common.cloudInstructions.passwordAndResetLink": "{passwordTemplate}が「Elastic」ユーザーのパスワードです。\\{#config.cloud.base_url\\}\\{#config.cloud.profile_url\\}\n パスワードを忘れた場合[Elastic Cloudでリセット] (\\{#config.cloud.base_url\\}\\{config.cloud.profile_url\\}) 。\n \\{#config.cloud.base_url\\}\\{/config.cloud.profile_url\\}", "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "はじめに", @@ -3027,7 +3027,6 @@ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高度な設定", "kibana-react.tableListView.listing.listingLimitExceededDescription": "{totalItems} 件の {entityNamePlural} がありますが、{listingLimitText} の設定により {listingLimitValue} 件までしか下の表に表示できません。{advancedSettingsLink} の下でこの設定を変更できます。", "kibana-react.tableListView.listing.listingLimitExceededTitle": "リスティング制限超過", - "kibana-react.tableListView.listing.noAvailableItemsMessage": "利用可能な {entityNamePlural} がありません。", "kibana-react.tableListView.listing.noMatchedItemsMessage": "検索条件に一致する {entityNamePlural} がありません。", "kibana-react.tableListView.listing.table.actionTitle": "アクション", "kibana-react.tableListView.listing.table.editActionDescription": "編集", @@ -3081,7 +3080,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子はdata-updateに対応できるようこのメソッドを導入する必要があります", "maps_legacy.defaultDistributionMessage": "Mapsを入手するには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", - "maps_legacy.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、ElasticsearchとKibanaの{defaultDistribution}にアップグレードしてください。{ems}ではより多くのズームレベルを無料で利用できます。または、独自のマップサーバーを構成できます。詳細は、{ wms }または{ configSettings}をご覧ください。", "maps_legacy.legacyMapDeprecationMessage": "Mapsを使用すると、複数のレイヤーとインデックスを追加する、個別のドキュメントをプロットする、データ値から特徴を表現する、ヒートマップ、グリッド、クラスターを追加するなど、さまざまなことが可能です。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "{label}は8.0でMapsに移行されます。", "maps_legacy.openInMapsButtonLabel": "Mapsで表示", @@ -3455,7 +3453,6 @@ "timelion.help.configuration.valid.paragraph1Part2": "で Elasticsearch データソースの構成に関する詳細をご覧ください。", "timelion.help.configuration.valid.paragraph2": "すでにチャートが 1 つ表示されていますが、興味深いデータを得るにはいくつか調整が必要な可能性があります。", "timelion.help.configuration.valid.paragraph3": "これで、一定期間のデータポイントの数を示す折れ線グラフが表示されるはずです。", - "timelion.help.configuration.valid.timeRangeText": "Kibana ツールバーのタイムピッカーで可視化するデータを含む期間を選択します。上記のすべてまたは一部の時間範囲を含む時間範囲を選択するようにしてください。", "timelion.help.configuration.valid.timeRangeTitle": "時間範囲", "timelion.help.configuration.validTitle": "良いお知らせです。Elasticsearch が正しく構成されました!", "timelion.help.dataTransforming.functionReferenceLinkText": "機能リファレンス", @@ -3465,7 +3462,6 @@ "timelion.help.dataTransforming.paragraph4": "まぁまぁですが、これでは 0 から 1 までの値になってしまいます。パーセンテージに変換するには、100 を掛けます:{multiplyDataQuery}。", "timelion.help.dataTransforming.paragraph5": "これでトラフィックの何パーセントが米国からのものなのか分かり、一定期間内にどのように変化したのか見ることができます!Timelion には、{sum}、{subtract}、{multiply}、{divide} などのいくつもの演算機能が搭載されています。これらの多くが数列や数字を扱えます。また、{movingaverage}、{abs}、{derivative} といった他の便利な変換機能もあります。", "timelion.help.dataTransforming.paragraph6Part1": "構文を学んだところで、", - "timelion.help.dataTransforming.paragraph6Part2": "Timelion で利用できるすべての機能の使い方をご覧ください。Kibana ツールバーの \\{Docs\\} をクリックしていつでもリファレンスを参照することができます。このチュートリアルに戻るには、リファレンスの上にある \\{Tutorial\\} リンクをクリックします。", "timelion.help.dataTransformingTitle": "データの変換:お楽しみの始まりです!", "timelion.help.dontShowHelpButtonLabel": "今後表示しない", "timelion.help.expressions.examples.customStylingDescription": "{descriptionTitle}初めの数列を赤くし、2 つ目の数列に 1 ピクセル幅のバーを使用します。", @@ -3479,7 +3475,6 @@ "timelion.help.expressions.functionReferenceLinkText": "機能リファレンス", "timelion.help.expressions.paragraph1": "それぞれの式はデータソース関数で始まります。ここから、新しい関数をデータソースに追加して変換や強化ができます。", "timelion.help.expressions.paragraph2": "ところで、ここから先はデータの持ち主が一番よくご存知なのではないでしょうか。サンプルクエリをより有意義なものと自由に置き換えてみてください。", - "timelion.help.expressions.paragraph3": "Kibana ツールバーの {strongAdd} をクリックして、他のチャートをいくつか追加してみましょう。そして、チャートを選択して次の式の内の 1 つをコピーし、インプットバーに貼り付けて、Enter を押します。リセットして繰り返し、他の式を試してみましょう。", "timelion.help.expressions.paragraph4": "Timelion は、チャートの見た目をカスタマイズするための他のビュー変換機能も搭載しています。完全なリストは次のリソースをご覧ください", "timelion.help.expressions.strongAddText": "追加", "timelion.help.expressionsTitle": "式を使って式を定義", @@ -4544,7 +4539,6 @@ "visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "ビジュアライゼーションへの変更が適用されました。", "visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "最新の変更が適用されました。", "visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "変更が自動的に適用されます。", - "visTypeTimeseries.visEditorVisualization.panelInterval": "間隔:{panelInterval}", "visTypeTimeseries.visPicker.gaugeLabel": "ゲージ", "visTypeTimeseries.visPicker.metricLabel": "メトリック", "visTypeTimeseries.visPicker.tableLabel": "表", @@ -8175,10 +8169,6 @@ "xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "インデックス名またはパターンはすでに存在します。", "xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "インデックス名に許可されていない文字が含まれています。", "xpack.fileUpload.indexSettings.indexNameGuidelines": "インデックス名ガイドライン", - "xpack.fileUpload.jsonImport.indexingResponse": "インデックス応答", - "xpack.fileUpload.jsonImport.indexMgmtLink": "インデックス管理", - "xpack.fileUpload.jsonImport.indexModsMsg": "次を使用すると、その他のインデックス修正を行うことができます。\n", - "xpack.fileUpload.jsonImport.indexPatternResponse": "インデックスパターン応答", "xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "データインデックスエラー", "xpack.fileUpload.jsonUploadAndParse.indexPatternError": "インデックスパターンエラー", "xpack.fleet.agentBulkActions.clearSelection": "選択した項目をクリア", @@ -12580,7 +12570,6 @@ "xpack.maps.mapListing.descriptionFieldTitle": "説明", "xpack.maps.mapListing.entityName": "マップ", "xpack.maps.mapListing.entityNamePlural": "マップ", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません", "xpack.maps.mapListing.titleFieldTitle": "タイトル", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択", "xpack.maps.mapSavedObjectLabel": "マップ", @@ -12768,7 +12757,6 @@ "xpack.maps.source.esSearch.topHitsSplitFieldLabel": "エンティティ", "xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", "xpack.maps.source.esSearch.useMVTVectorTiles": "ベクトルタイルを使用", - "xpack.maps.source.esSearch.useTopHitsLabel": "エンティティごとにトップヒットを表示。", "xpack.maps.source.esSearchDescription": "Elasticsearch の点、線、多角形", "xpack.maps.source.esSearchTitle": "ドキュメント", "xpack.maps.source.esSource.noGeoFieldErrorMessage": "インデックスパターン {indexPatternTitle} には現在ジオフィールド {geoField} が含まれていません", @@ -12812,8 +12800,6 @@ "xpack.maps.source.pewPewDescription": "ソースとデスティネーションの間の集約データパスです。", "xpack.maps.source.pewPewTitle": "ソースとデスティネーションの接続", "xpack.maps.source.urlLabel": "Url", - "xpack.maps.source.vetorSource.formatErrorMessage": "URL からベクターシェイプを取得できません:{format}", - "xpack.maps.source.vetorSource.requestFailedErrorMessage": "URL からベクターシェイプを取得できません:{fetchUrl}", "xpack.maps.source.wms.attributionLink": "属性テキストにはリンクが必要です", "xpack.maps.source.wms.attributionText": "属性 URL にはテキストが必要です", "xpack.maps.source.wms.getCapabilitiesButtonText": "負荷容量", @@ -13338,7 +13324,6 @@ "xpack.ml.dataframe.analytics.create.etaInputAriaLabel": "縮小が重みに適用されました。", "xpack.ml.dataframe.analytics.create.etaLabel": "Eta", "xpack.ml.dataframe.analytics.create.etaText": "縮小が重みに適用されました。0.001から1の範囲でなければなりません。", - "xpack.ml.dataframe.analytics.create.extraUnsupportedRuntimeFieldsMsg": "{count}以上", "xpack.ml.dataframe.analytics.create.featureBagFractionInputAriaLabel": "各候補分割のランダムなbagを選択したときに使用される特徴量の割合", "xpack.ml.dataframe.analytics.create.featureBagFractionLabel": "特徴量bag割合", "xpack.ml.dataframe.analytics.create.featureBagFractionText": "各候補分割のランダムなbagを選択したときに使用される特徴量の割合。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 04962e1a56330..117c33a286d88 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2108,7 +2108,7 @@ "home.tutorials.common.auditbeatStatusCheck.successText": "已成功接收数据", "home.tutorials.common.auditbeatStatusCheck.text": "确认从 Auditbeat 收到数据", "home.tutorials.common.auditbeatStatusCheck.title": "状态", - "home.tutorials.common.cloudInstructions.passwordAndResetLink": "其中 {passwordTemplate} 是用户 `elastic` 的密码。\\{#config.cloud.resetPasswordUrl\\}\n 忘了密码?[在 Elastic Cloud 中重置](\\{config.cloud.resetPasswordUrl\\})。\n \\{/config.cloud.resetPasswordUrl\\}", + "home.tutorials.common.cloudInstructions.passwordAndResetLink": "其中 {passwordTemplate} 是用户 `elastic` 的密码。\\{#config.cloud.base_url\\}\\{#config.cloud.profile_url\\}\n 忘了密码?[在 Elastic Cloud 中重置](\\{#config.cloud.base_url\\}\\{config.cloud.profile_url\\})。\n \\{#config.cloud.base_url\\}\\{/config.cloud.profile_url\\}", "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "入门", @@ -3048,7 +3048,6 @@ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高级设置", "kibana-react.tableListView.listing.listingLimitExceededDescription": "您有 {totalItems} 个{entityNamePlural},但您的“{listingLimitText}”设置阻止下表显示 {listingLimitValue} 个以上。您可以在“{advancedSettingsLink}”下更改此设置。", "kibana-react.tableListView.listing.listingLimitExceededTitle": "已超过列表限制", - "kibana-react.tableListView.listing.noAvailableItemsMessage": "没有可用的{entityNamePlural}。", "kibana-react.tableListView.listing.noMatchedItemsMessage": "没有任何{entityNamePlural}匹配您的搜索。", "kibana-react.tableListView.listing.table.actionTitle": "操作", "kibana-react.tableListView.listing.table.editActionDescription": "编辑", @@ -3102,7 +3101,6 @@ "maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子对象应实现此方法以响应数据更新", "maps_legacy.defaultDistributionMessage": "要获取 Maps,请升级到 {defaultDistribution} 版的 Elasticsearch 和 Kibana。", "maps_legacy.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", - "maps_legacy.kibanaMap.zoomWarning": "已达到缩放级别数目上限。要一直放大,请升级到 Elasticsearch 和 Kibana 的{defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", "maps_legacy.legacyMapDeprecationMessage": "使用 Maps,可以添加多个图层和索引,绘制单个文档,使用数据值表示特征,添加热图、网格和集群,等等。{getMapsMessage}", "maps_legacy.legacyMapDeprecationTitle": "在 8.0 中,{label} 将迁移到 Maps。", "maps_legacy.openInMapsButtonLabel": "在 Maps 中查看", @@ -4570,7 +4568,6 @@ "visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "尚未应用对此可视化的更改。", "visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "已应用最新更改。", "visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "将自动应用更改。", - "visTypeTimeseries.visEditorVisualization.panelInterval": "时间间隔:{panelInterval}", "visTypeTimeseries.visPicker.gaugeLabel": "仪表盘", "visTypeTimeseries.visPicker.metricLabel": "指标", "visTypeTimeseries.visPicker.tableLabel": "表", @@ -8245,10 +8242,6 @@ "xpack.fileUpload.indexSettings.indexNameAlreadyExistsErrorMessage": "索引名称或模式已存在。", "xpack.fileUpload.indexSettings.indexNameContainsIllegalCharactersErrorMessage": "索引名称包含非法字符。", "xpack.fileUpload.indexSettings.indexNameGuidelines": "索引名称指引", - "xpack.fileUpload.jsonImport.indexingResponse": "索引响应", - "xpack.fileUpload.jsonImport.indexMgmtLink": "索引管理", - "xpack.fileUpload.jsonImport.indexModsMsg": "要进一步做索引修改,可以使用\n", - "xpack.fileUpload.jsonImport.indexPatternResponse": "索引模式响应", "xpack.fileUpload.jsonUploadAndParse.dataIndexingError": "数据索引错误", "xpack.fileUpload.jsonUploadAndParse.indexPatternError": "索引模式错误", "xpack.fleet.agentBulkActions.agentsSelected": "已选择 {count, plural, other {# 个代理}}", @@ -12745,7 +12738,6 @@ "xpack.maps.mapListing.descriptionFieldTitle": "描述", "xpack.maps.mapListing.entityName": "地图", "xpack.maps.mapListing.entityNamePlural": "地图", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图", "xpack.maps.mapListing.titleFieldTitle": "标题", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式", "xpack.maps.mapSavedObjectLabel": "地图", @@ -12933,7 +12925,6 @@ "xpack.maps.source.esSearch.topHitsSplitFieldLabel": "实体", "xpack.maps.source.esSearch.topHitsSplitFieldSelectPlaceholder": "选择实体字段", "xpack.maps.source.esSearch.useMVTVectorTiles": "使用矢量磁贴", - "xpack.maps.source.esSearch.useTopHitsLabel": "显示每个实体最高命中结果。", "xpack.maps.source.esSearchDescription": "Elasticsearch 的点、线和多边形", "xpack.maps.source.esSearchTitle": "文档", "xpack.maps.source.esSource.noGeoFieldErrorMessage": "索引模式“{indexPatternTitle}”不再包含地理字段 {geoField}", @@ -12977,8 +12968,6 @@ "xpack.maps.source.pewPewDescription": "源和目标之间的聚合数据路径", "xpack.maps.source.pewPewTitle": "源-目标连接", "xpack.maps.source.urlLabel": "URL", - "xpack.maps.source.vetorSource.formatErrorMessage": "无法从以下 URL 获取矢量形状:{format}", - "xpack.maps.source.vetorSource.requestFailedErrorMessage": "无法从以下 URL 获取矢量形状:{fetchUrl}", "xpack.maps.source.wms.attributionLink": "属性文本必须附带链接", "xpack.maps.source.wms.attributionText": "属性 url 必须附带文本", "xpack.maps.source.wms.getCapabilitiesButtonText": "加载功能", @@ -13506,7 +13495,6 @@ "xpack.ml.dataframe.analytics.create.etaInputAriaLabel": "缩小量已应用于权重。", "xpack.ml.dataframe.analytics.create.etaLabel": "Eta", "xpack.ml.dataframe.analytics.create.etaText": "缩小量已应用于权重。必须介于 0.001 和 1 之间。", - "xpack.ml.dataframe.analytics.create.extraUnsupportedRuntimeFieldsMsg": "及另外 {count} 个", "xpack.ml.dataframe.analytics.create.featureBagFractionInputAriaLabel": "选择为每个候选拆分选择随机袋时使用的特征比例", "xpack.ml.dataframe.analytics.create.featureBagFractionLabel": "特征袋比例", "xpack.ml.dataframe.analytics.create.featureBagFractionText": "选择为每个候选拆分选择随机袋时使用的特征比例。", @@ -13610,7 +13598,6 @@ "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.create.unableToFetchExplainDataMessage": "提取分析字段数据时发生错误。", "xpack.ml.dataframe.analytics.create.unsupportedFieldsError": "无效。{message}", - "xpack.ml.dataframe.analytics.create.unsupportedRuntimeFieldsCallout": "不支持分析运行时{runtimeFieldsCount, plural, other {字段}} {unsupportedRuntimeFields} {extraCountMsg}。", "xpack.ml.dataframe.analytics.create.useEstimatedMmlLabel": "使用估计的模型内存限制", "xpack.ml.dataframe.analytics.create.UseResultsFieldDefaultLabel": "使用结果字段默认值“{defaultValue}”", "xpack.ml.dataframe.analytics.create.viewResultsCardDescription": "查看分析作业的结果。", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index e3457884594a9..7ea6b72547386 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -68,18 +68,21 @@ export class UptimePlugin return UptimeDataHelper(coreStart); }; - plugins.observability.dashboard.register({ - appName: 'uptime', - hasData: async () => { - const dataHelper = await getUptimeDataHelper(); - const status = await dataHelper.indexStatus(); - return status.docCount > 0; - }, - fetchData: async (params: FetchDataParams) => { - const dataHelper = await getUptimeDataHelper(); - return await dataHelper.overviewData(params); - }, - }); + + if (plugins.observability) { + plugins.observability.dashboard.register({ + appName: 'uptime', + hasData: async () => { + const dataHelper = await getUptimeDataHelper(); + const status = await dataHelper.indexStatus(); + return status.docCount > 0; + }, + fetchData: async (params: FetchDataParams) => { + const dataHelper = await getUptimeDataHelper(); + return await dataHelper.overviewData(params); + }, + }); + } core.application.register({ id: PLUGIN.ID, diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx index f2da38091e37f..6706a435c7b6b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx @@ -26,7 +26,7 @@ import { import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; import { MapToolTipComponent } from './map_tool_tip'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; +import type { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; export interface EmbeddedMapProps { upPoints: LocationPoint[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx index f2d1227fe870e..c03ed94f8c544 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx @@ -22,8 +22,7 @@ import { AppState } from '../../../../../state'; import { monitorLocationsSelector } from '../../../../../state/selectors'; import { useMonitorId } from '../../../../../hooks'; import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; +import type { RenderTooltipContentParams } from '../../../../../../../maps/public'; import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; import { LastCheckLabel } from '../../translations'; diff --git a/x-pack/scripts/jest.js b/x-pack/scripts/jest.js index 4c83073a559a4..2ea950e075c8c 100644 --- a/x-pack/scripts/jest.js +++ b/x-pack/scripts/jest.js @@ -5,4 +5,5 @@ * 2.0. */ +require('../../src/setup_node_env/ensure_node_preserve_symlinks'); require('@kbn/test').runJest(); diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 59ce697811aa7..a8d20ff56de08 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -112,8 +112,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // Skip until https://github.com/elastic/kibana/issues/88661 gets closed - it.skip('lens XY chart with multiple layers', async () => { + it('lens XY chart with multiple layers', async () => { await PageObjects.lens.createLayer(); await PageObjects.lens.switchToVisualization('area'); diff --git a/x-pack/test/api_integration/apis/management/rollup/rollup.js b/x-pack/test/api_integration/apis/management/rollup/rollup.js index 4cb2ef6ea0fa0..699592fd99920 100644 --- a/x-pack/test/api_integration/apis/management/rollup/rollup.js +++ b/x-pack/test/api_integration/apis/management/rollup/rollup.js @@ -56,10 +56,16 @@ export default function ({ getService }) { expect(body.doesMatchIndices).to.be(true); expect(body.doesMatchRollupIndices).to.be(false); expect(body.dateFields).to.eql(['testCreatedField']); - expect(body.keywordFields).to.eql(['testTagField']); - - // Allowing the test to account for future addition of doc_count - expect(body.numericFields.indexOf('testTotalField')).to.be.greaterThan(-1); + // '_tier' is an expected metadata field from ES + // Order is not guaranteed, so we assert against individual field names + ['_tier', 'testTagField'].forEach((keywordField) => { + expect(body.keywordFields.includes(keywordField)).to.be(true); + }); + // '_doc_count' is an expected metadata field from ES + // Order is not guaranteed, so we assert against individual field names + ['_doc_count', 'testTotalField'].forEach((numericField) => { + expect(body.numericFields.includes(numericField)).to.be(true); + }); }); it("should not return any fields when the index pattern doesn't match any indices", async () => { diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts index 06ea5dc800e45..9b4d39a3b10b3 100644 --- a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts @@ -14,7 +14,7 @@ interface SlmPolicy { repository: string; isManagedPolicy: boolean; config?: { - indices?: string | string[]; + indices: string | string[]; ignoreUnavailable?: boolean; includeGlobalState?: boolean; partial?: boolean; @@ -36,19 +36,21 @@ interface SlmPolicy { export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { let policiesCreated: string[] = []; - const es = getService('legacyEs'); + const es = getService('es'); const createRepository = (repoName: string) => { - return es.snapshot.createRepository({ - repository: repoName, - body: { - type: 'fs', - settings: { - location: '/tmp/', + return es.snapshot + .createRepository({ + repository: repoName, + body: { + type: 'fs', + settings: { + location: '/tmp/', + }, }, - }, - verify: false, - }); + verify: false, + }) + .then(({ body }) => body); }; const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => { @@ -56,20 +58,27 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService']) policiesCreated.push(policy.name); } - return es.sr.updatePolicy({ - name: policy.name, - body: policy, - }); + return es.slm + .putLifecycle({ + policy_id: policy.name, + // TODO: bring {@link SlmPolicy} in line with {@link PutSnapshotLifecycleRequest['body']} + // @ts-expect-error + body: policy, + }) + .then(({ body }) => body); }; const getPolicy = (policyName: string) => { - return es.sr.policy({ - name: policyName, - human: true, - }); + return es.slm + .getLifecycle({ + policy_id: policyName, + human: true, + }) + .then(({ body }) => body); }; - const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName }); + const deletePolicy = (policyName: string) => + es.slm.deleteLifecycle({ policy_id: policyName }).then(({ body }) => body); const cleanupPolicies = () => Promise.all(policiesCreated.map(deletePolicy)) diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index 50bc85ed1e793..63a6a842fd9f7 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -14,6 +14,7 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const retry = getService('retry'); + const spacesService = getService('spaces'); describe('search session', () => { describe('session management', () => { @@ -596,5 +597,157 @@ export default function ({ getService }: FtrProviderContext) { .expect(403); }); }); + + describe('in non-default space', () => { + const spaceId = 'foo-space'; + before(async () => { + try { + await spacesService.create({ + id: spaceId, + name: 'Foo Space', + }); + } catch { + // might already be created + } + }); + + after(async () => { + await spacesService.delete(spaceId); + }); + + it('should complete and delete non-persistent sessions', async () => { + const sessionId = `my-session-${Math.random()}`; + + // run search + const searchRes = await supertest + .post(`/s/${spaceId}/internal/search/ese`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + params: { + body: { + query: { + term: { + agent: '1', + }, + }, + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = searchRes.body; + + await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + const resp = await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + const { touched, created, persisted, idMapping } = resp.body.attributes; + expect(persisted).to.be(false); + expect(touched).not.to.be(undefined); + expect(created).not.to.be(undefined); + + const idMappings = Object.values(idMapping).map((value: any) => value.id); + expect(idMappings).to.contain(id); + return true; + }); + + // not touched timeout in tests is 15s, wait to give a chance for status to update + await new Promise((resolve) => + setTimeout(() => { + resolve(void 0); + }, 15_000) + ); + + await retry.waitForWithTimeout( + 'searches eventually complete and session gets into the complete state', + 30_000, + async () => { + await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(404); + + return true; + } + ); + }); + + it('should complete persisten session', async () => { + const sessionId = `my-session-${Math.random()}`; + + // run search + const searchRes = await supertest + .post(`/s/${spaceId}/internal/search/ese`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + params: { + body: { + query: { + term: { + agent: '1', + }, + }, + }, + wait_for_completion_timeout: '1ms', + }, + }) + .expect(200); + + const { id } = searchRes.body; + + // persist session + await supertest + .post(`/s/${spaceId}/internal/session`) + .set('kbn-xsrf', 'foo') + .send({ + sessionId, + name: 'My Session', + appId: 'discover', + expires: '123', + urlGeneratorId: 'discover', + }) + .expect(200); + + await retry.waitForWithTimeout('searches persisted into session', 5000, async () => { + const resp = await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + const { touched, created, persisted, idMapping } = resp.body.attributes; + expect(persisted).to.be(true); + expect(touched).not.to.be(undefined); + expect(created).not.to.be(undefined); + + const idMappings = Object.values(idMapping).map((value: any) => value.id); + expect(idMappings).to.contain(id); + return true; + }); + + // session refresh interval is 5 seconds, wait to give a chance for status to update + await new Promise((resolve) => setTimeout(resolve, 5000)); + + await retry.waitForWithTimeout( + 'searches eventually complete and session gets into the complete state', + 5000, + async () => { + const resp = await supertest + .get(`/s/${spaceId}/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo') + .expect(200); + + const { status } = resp.body.attributes; + + expect(status).to.be(SearchSessionStatus.COMPLETE); + return true; + } + ); + }); + }); }); } diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 49482292bfb25..0b02d394b107f 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -10,7 +10,6 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; -import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -21,6 +20,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin], + plugins: [indexManagementEsClientPlugin], }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts new file mode 100644 index 0000000000000..80c2b98266248 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/errors/failed_transactions`, + query: { + start: range.start, + end: range.end, + fieldNames: 'user_agent.name,user_agent.os.name,url.original', + }, + }); + registry.when( + 'correlations errors failed transactions without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations errors failed transactions with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/failed_transactions'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + const { significantTerms } = response.body; + expect(significantTerms).to.have.length(2); + const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); + expectSnapshot(sortedFieldNames).toMatchInline(` + Array [ + "user_agent.name", + "user_agent.name", + ] + `); + }); + + it('returns a distribution per term', () => { + const { significantTerms } = response.body; + expectSnapshot(significantTerms.map((term) => term.timeseries.length)).toMatchInline(` + Array [ + 31, + 31, + ] + `); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts new file mode 100644 index 0000000000000..206da2968b4c1 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/errors/overall_timeseries`, + query: { + start: range.start, + end: range.end, + }, + }); + + registry.when( + 'correlations errors overall without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations errors overall with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns overall distribution', () => { + expectSnapshot(response.body?.overall?.timeseries.length).toMatchInline(`31`); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts new file mode 100644 index 0000000000000..0d79333faa9ef --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/latency/overall_distribution`, + query: { + start: range.start, + end: range.end, + }, + }); + + registry.when( + 'correlations latency overall without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations latency overall with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/overall_distribution'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns overall distribution', () => { + expectSnapshot(response.body?.distributionInterval).toMatchInline(`238776`); + expectSnapshot(response.body?.maxLatency).toMatchInline(`3581640.00000003`); + expectSnapshot(response.body?.overallDistribution?.length).toMatchInline(`15`); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts new file mode 100644 index 0000000000000..d32beee0f31d5 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts @@ -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 expect from '@kbn/expect'; +import { format } from 'url'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const archiveName = 'apm_8.0.0'; + const range = archives_metadata[archiveName]; + + const url = format({ + pathname: `/api/apm/correlations/latency/slow_transactions`, + query: { + start: range.start, + end: range.end, + durationPercentile: 95, + fieldNames: 'user_agent.name,user_agent.os.name,url.original', + maxLatency: 3581640.00000003, + distributionInterval: 238776, + }, + }); + registry.when( + 'correlations latency slow transactions without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await supertest.get(url); + + expect(response.status).to.be(200); + expect(response.body.response).to.be(undefined); + }); + } + ); + + registry.when( + 'correlations latency slow transactions with data and default args', + { config: 'trial', archives: ['apm_8.0.0'] }, + () => { + type ResponseBody = APIReturnType<'GET /api/apm/correlations/latency/slow_transactions'>; + let response: { + status: number; + body: NonNullable; + }; + + before(async () => { + response = await supertest.get(url); + }); + + it('returns successfully', () => { + expect(response.status).to.eql(200); + }); + + it('returns significant terms', () => { + const { significantTerms } = response.body; + expect(significantTerms).to.have.length(9); + const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); + expectSnapshot(sortedFieldNames).toMatchInline(` + Array [ + "url.original", + "url.original", + "url.original", + "url.original", + "user_agent.name", + "user_agent.name", + "user_agent.name", + "user_agent.os.name", + "user_agent.os.name", + ] + `); + }); + + it('returns a distribution per term', () => { + const { significantTerms } = response.body; + expectSnapshot(significantTerms.map((term) => term.distribution.length)).toMatchInline(` + Array [ + 15, + 15, + 15, + 15, + 15, + 15, + 15, + 15, + 15, + ] + `); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts deleted file mode 100644 index c9686a8a9d5b0..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/slow_transactions.ts +++ /dev/null @@ -1,96 +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 expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { registry } from '../../common/registry'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const archiveName = 'apm_8.0.0'; - const range = archives_metadata[archiveName]; - - const url = format({ - pathname: `/api/apm/correlations/slow_transactions`, - query: { - start: range.start, - end: range.end, - durationPercentile: 95, - fieldNames: 'user_agent.name,user_agent.os.name,url.original', - }, - }); - - registry.when('without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); - }); - }); - - registry.when('with data and default args', { config: 'trial', archives: ['apm_8.0.0'] }, () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>; - let response: { - status: number; - body: NonNullable; - }; - - before(async () => { - response = await supertest.get(url); - }); - - it('returns successfully', () => { - expect(response.status).to.eql(200); - }); - - it('returns significant terms', () => { - const significantTerms = response.body?.significantTerms as NonNullable< - typeof response.body.significantTerms - >; - expect(significantTerms).to.have.length(9); - const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); - expectSnapshot(sortedFieldNames).toMatchInline(` - Array [ - "url.original", - "url.original", - "url.original", - "url.original", - "user_agent.name", - "user_agent.name", - "user_agent.name", - "user_agent.os.name", - "user_agent.os.name", - ] - `); - }); - - it('returns a distribution per term', () => { - expectSnapshot(response.body?.significantTerms?.map((term) => term.distribution.length)) - .toMatchInline(` - Array [ - 15, - 15, - 15, - 15, - 15, - 15, - 15, - 15, - 15, - ] - `); - }); - - it('returns overall distribution', () => { - expectSnapshot(response.body?.overall?.distribution.length).toMatchInline(`15`); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 9f0f1b15c0580..7c69d5b996cea 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -24,8 +24,20 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./alerts/chart_preview')); }); - describe('correlations/slow_transactions', function () { - loadTestFile(require.resolve('./correlations/slow_transactions')); + describe('correlations/latency_slow_transactions', function () { + loadTestFile(require.resolve('./correlations/latency_slow_transactions')); + }); + + describe('correlations/latency_overall', function () { + loadTestFile(require.resolve('./correlations/latency_overall')); + }); + + describe('correlations/errors_overall', function () { + loadTestFile(require.resolve('./correlations/errors_overall')); + }); + + describe('correlations/errors_failed_transactions', function () { + loadTestFile(require.resolve('./correlations/errors_failed_transactions')); }); describe('metrics_charts/metrics_charts', function () { diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts index 2af5f51953b23..746e746ccd827 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts @@ -33,7 +33,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(response.body.serviceCount).to.be(0); - expect(response.body.transactionCoordinates.length).to.be(0); + expect(response.body.transactionPerMinute.timeseries.length).to.be(0); }); }); } @@ -50,14 +50,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expect(response.body.serviceCount).to.be.greaterThan(0); - expect(response.body.transactionCoordinates.length).to.be.greaterThan(0); + expect(response.body.transactionPerMinute.timeseries.length).to.be.greaterThan(0); expectSnapshot(response.body.serviceCount).toMatchInline(`9`); - expectSnapshot(response.body.transactionCoordinates.length).toMatchInline(`31`); + expectSnapshot(response.body.transactionPerMinute.value).toMatchInline(`64.8`); + expectSnapshot(response.body.transactionPerMinute.timeseries.length).toMatchInline(`31`); expectSnapshot( - response.body.transactionCoordinates + response.body.transactionPerMinute.timeseries .slice(0, 5) .map(({ x, y }: { x: number; y: number }) => ({ x: new Date(x).toISOString(), @@ -67,23 +68,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ Object { "x": "2020-12-08T13:57:00.000Z", - "y": 0.166666666666667, + "y": 2, }, Object { "x": "2020-12-08T13:58:00.000Z", - "y": 5.23333333333333, + "y": 61, }, Object { "x": "2020-12-08T13:59:00.000Z", - "y": 4.4, + "y": 36, }, Object { "x": "2020-12-08T14:00:00.000Z", - "y": 5.73333333333333, + "y": 75, }, Object { "x": "2020-12-08T14:01:00.000Z", - "y": 4.33333333333333, + "y": 36, }, ] `); diff --git a/x-pack/test/banners_functional/config.ts b/x-pack/test/banners_functional/config.ts new file mode 100644 index 0000000000000..21cce31ca5d85 --- /dev/null +++ b/x-pack/test/banners_functional/config.ts @@ -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 path from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services, pageObjects } from './ftr_provider_context'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + testFiles: [require.resolve('./tests')], + servers: { + ...kibanaFunctionalConfig.get('servers'), + }, + services, + pageObjects, + + junit: { + reportName: 'X-Pack Banners Functional Tests', + }, + + esTestCluster: kibanaFunctionalConfig.get('esTestCluster'), + apps: { + ...kibanaFunctionalConfig.get('apps'), + }, + + esArchiver: { + directory: path.resolve(__dirname, '..', 'functional', 'es_archives'), + }, + + kbnTestServer: { + ...kibanaFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.banners.placement=header', + '--xpack.banners.textContent="global banner text"', + ], + }, + }; +} diff --git a/x-pack/test/banners_functional/ftr_provider_context.ts b/x-pack/test/banners_functional/ftr_provider_context.ts new file mode 100644 index 0000000000000..faac2954b00f6 --- /dev/null +++ b/x-pack/test/banners_functional/ftr_provider_context.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { services } from '../functional/services'; +import { pageObjects } from '../functional/page_objects'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { services, pageObjects }; diff --git a/x-pack/test/banners_functional/tests/global.ts b/x-pack/test/banners_functional/tests/global.ts new file mode 100644 index 0000000000000..cef404d7ed132 --- /dev/null +++ b/x-pack/test/banners_functional/tests/global.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security', 'banners']); + + describe('global pages', () => { + it('displays the global banner on the login page', async () => { + await PageObjects.common.navigateToApp('login'); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('global banner text'); + }); + }); +} diff --git a/x-pack/test/banners_functional/tests/index.ts b/x-pack/test/banners_functional/tests/index.ts new file mode 100644 index 0000000000000..301c872c746e1 --- /dev/null +++ b/x-pack/test/banners_functional/tests/index.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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('banners - functional tests', function () { + this.tags('ciGroup2'); + + loadTestFile(require.resolve('./global')); + loadTestFile(require.resolve('./spaces')); + }); +} diff --git a/x-pack/test/banners_functional/tests/spaces.ts b/x-pack/test/banners_functional/tests/spaces.ts new file mode 100644 index 0000000000000..f8c412c0df0e3 --- /dev/null +++ b/x-pack/test/banners_functional/tests/spaces.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects([ + 'common', + 'security', + 'banners', + 'settings', + 'spaceSelector', + ]); + + describe('per-spaces banners', () => { + before(async () => { + await esArchiver.load('banners/multispace'); + }); + + after(async () => { + await esArchiver.unload('banners/multispace'); + }); + + before(async () => { + await PageObjects.security.login(undefined, undefined, { + expectSpaceSelector: true, + }); + await PageObjects.spaceSelector.clickSpaceCard('default'); + + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + + await PageObjects.settings.setAdvancedSettingsTextArea( + 'banners:textContent', + 'default space banner text' + ); + }); + + it('displays the space-specific banner within the space', async () => { + await PageObjects.common.navigateToApp('home'); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('default space banner text'); + }); + + it('displays the global banner within another space', async () => { + await PageObjects.common.navigateToApp('home', { basePath: '/s/another-space' }); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('global banner text'); + }); + + it('displays the global banner on the login page', async () => { + await PageObjects.security.forceLogout(); + await PageObjects.common.navigateToApp('login'); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('global banner text'); + }); + }); +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index a7925fa756693..f0b173d2d4c48 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -317,6 +317,16 @@ export default ({ getService }: FtrProviderContext) => { { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, first_seen: '2021-01-26T11:09:04.000Z', matched: { atomic: '159.89.119.67', @@ -339,6 +349,16 @@ export default ({ getService }: FtrProviderContext) => { { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, first_seen: '2021-01-26T11:09:04.000Z', matched: { atomic: '159.89.119.67', @@ -412,6 +432,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on ip', @@ -426,6 +456,16 @@ export default ({ getService }: FtrProviderContext) => { }, provider: 'other_provider', type: 'ip', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, ]); }); @@ -492,6 +532,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, // We do not merge matched indicators during enrichment, so in // certain circumstances a given indicator document could appear @@ -512,6 +562,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on ip', @@ -526,6 +586,16 @@ export default ({ getService }: FtrProviderContext) => { }, provider: 'other_provider', type: 'ip', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, ]); }); @@ -600,6 +670,16 @@ export default ({ getService }: FtrProviderContext) => { full: 'http://159.89.119.67:59600/bin.sh', scheme: 'http', }, + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, }, ]); @@ -621,6 +701,16 @@ export default ({ getService }: FtrProviderContext) => { full: 'http://159.89.119.67:59600/bin.sh', scheme: 'http', }, + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on both port and ip', @@ -636,6 +726,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on both port and ip', @@ -651,6 +751,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, ]); }); diff --git a/x-pack/test/examples/config.ts b/x-pack/test/examples/config.ts index dd087772ae52c..fe1b5ce299447 100644 --- a/x-pack/test/examples/config.ts +++ b/x-pack/test/examples/config.ts @@ -8,6 +8,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { resolve } from 'path'; import fs from 'fs'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { KIBANA_ROOT } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 3245b9a459fb1..f79ff15b64d33 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -79,6 +79,7 @@ export default function ({ getService }: FtrProviderContext) { offline: 1, updating: 1, other: 1, + inactive: 0, }, }); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts index 38d139c59430e..3b32ea031f6e2 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts @@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { this.tags(['skipFirefox']); loadTestFile(require.resolve('./dashboard_security')); + loadTestFile(require.resolve('./time_to_visualize_security')); loadTestFile(require.resolve('./dashboard_spaces')); }); } diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts new file mode 100644 index 0000000000000..3ebc53cc7cf27 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -0,0 +1,233 @@ +/* + * Copyright 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'timeToVisualize', + 'timePicker', + 'dashboard', + 'visEditor', + 'visualize', + 'security', + 'common', + 'header', + 'lens', + ]); + + const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardExpect = getService('dashboardExpect'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const find = getService('find'); + + describe('dashboard time to visualize security', () => { + before(async () => { + await esArchiver.load('dashboard/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + + await security.role.create('dashboard_write_vis_read', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('dashboard_write_vis_read_user', { + password: 'dashboard_write_vis_read_user-password', + roles: ['dashboard_write_vis_read'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'dashboard_write_vis_read_user', + 'dashboard_write_vis_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('dashboard_write_vis_read'); + await security.user.delete('dashboard_write_vis_read_user'); + + await esArchiver.unload('dashboard/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('lens by value works without library save permissions', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('can add a lens panel by value', async () => { + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.lens.createAndAddLensFromDashboard({}); + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('edits to a by value lens panel are properly applied', async () => { + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.switchToVisualization('donut'); + await PageObjects.lens.saveAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); + + const pieExists = await find.existsByCssSelector('.lnsPieExpression__container'); + expect(pieExists).to.be(true); + }); + + it('disables save to library button without visualize save permissions', async () => { + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + const saveButton = await testSubjects.find('lnsApp_saveButton'); + expect(await saveButton.getAttribute('disabled')).to.equal('true'); + await PageObjects.lens.saveAndReturn(); + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new lens to be added by value, but not by reference', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox'); + expect(await libraryCheckbox.getAttribute('disabled')).to.equal('true'); + + await PageObjects.timeToVisualize.saveFromModal('New Lens from Modal', { + addToDashboard: 'new', + saveAsNew: true, + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'New Lens from Modal' + ); + expect(isLinked).to.be(false); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + }); + + describe('visualize by value works without library save permissions', () => { + const originalMarkdownText = 'Original markdown text'; + const modifiedMarkdownText = 'Modified markdown text'; + + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('can add a markdown panel by value', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.waitForRenderComplete(); + + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); + await PageObjects.visEditor.clickGo(); + + await PageObjects.visualize.saveVisualizationAndReturn(); + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('edits to a by value visualize panel are properly applied', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visEditor.setMarkdownTxt(modifiedMarkdownText); + await PageObjects.visEditor.clickGo(); + await PageObjects.visualize.saveVisualizationAndReturn(); + + await PageObjects.dashboard.waitForRenderComplete(); + const markdownText = await testSubjects.find('markdownBody'); + expect(await markdownText.getVisibleText()).to.eql(modifiedMarkdownText); + + const newPanelCount = PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('disables save to library button without visualize save permissions', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('visualizeSaveButton'); + await PageObjects.visualize.saveVisualizationAndReturn(); + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new visualization to be added by value, but not by reference', async function () { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + await PageObjects.visualize.ensureSavePanelOpen(); + const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox'); + expect(await libraryCheckbox.getAttribute('disabled')).to.equal('true'); + + await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', { + addToDashboard: 'new', + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,005']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index d4a909f6a0474..c437cfaa8f5dc 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -50,7 +50,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel }; - describe('Download CSV', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 + describe.skip('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await browser.setWindowSize(1600, 850); diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap index 5ddef936b41ae..baa49cb6f9d81 100644 --- a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -71,7 +71,7 @@ exports[`discover Discover CSV Export Generate CSV: new search generates a repor 24.5 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan " `; @@ -83,6 +83,6 @@ exports[`discover Discover CSV Export Generate CSV: new search generates a repor 24.5 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan " `; diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 9acb4c311c113..d7dd961e2f103 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -21,8 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': setValue }); }; - // Failing: See https://github.com/elastic/kibana/issues/95592 - describe.skip('Discover CSV Export', () => { + describe('Discover CSV Export', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.load('reporting/ecommerce'); diff --git a/x-pack/test/functional/apps/saved_objects_management/exports/_7.12_import_saved_objects.ndjson b/x-pack/test/functional/apps/saved_objects_management/exports/_7.12_import_saved_objects.ndjson new file mode 100644 index 0000000000000..5fe0c303668db --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/exports/_7.12_import_saved_objects.ndjson @@ -0,0 +1,34 @@ +{"attributes":{"fieldAttrs":"{\"machine.os\":{\"count\":1},\"spaces\":{\"count\":1},\"type\":{\"count\":1},\"bytes_scripted\":{\"count\":1}}","fields":"[{\"count\":1,\"script\":\"doc['bytes'].value*1024\",\"lang\":\"painless\",\"name\":\"bytes_scripted\",\"type\":\"number\",\"scripted\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]","runtimeFieldMap":"{}","timeFieldName":"@timestamp","title":"logstash-*"},"coreMigrationVersion":"7.12.1","id":"56b34100-619d-11eb-aebf-c306684b328d","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1617218924557,0],"type":"index-pattern","updated_at":"2021-03-31T19:28:44.557Z","version":"WzksMV0="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_scriptedfieldviz","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}}","version":1,"visState":"{\"title\":\"logstash_scriptedfieldviz\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes_scripted\",\"ranges\":[{\"from\":0,\"to\":40000},{\"from\":40001,\"to\":20000000}]}}]}"},"coreMigrationVersion":"7.12.1","id":"0a274320-61cc-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218952314,184],"type":"visualization","updated_at":"2021-03-31T19:29:12.314Z","version":"WzY3LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_datatable","uiStateJSON":"{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}","version":1,"visState":"{\"title\":\"logstash_datatable\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":true,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"bucket\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"response.raw\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"0d8a8860-623a-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218938927,33],"type":"visualization","updated_at":"2021-03-31T19:28:58.927Z","version":"WzM5LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_area_chart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_area_chart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2010-01-28T19:25:55.242Z\",\"to\":\"2021-01-28T19:40:55.242Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"machine OS\"}}]}"},"coreMigrationVersion":"7.12.1","id":"36b91810-6239-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218930707,21],"type":"visualization","updated_at":"2021-03-31T19:28:50.707Z","version":"WzIzLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_horizontal","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_horizontal\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"no of documents\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"no of documents\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"no of documents\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"extension.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"e4aef350-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218932758,19],"type":"visualization","updated_at":"2021-03-31T19:28:52.758Z","version":"WzI3LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_linechart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_linechart\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":51,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-09-18T06:38:43.311Z\",\"to\":\"2015-09-26T04:02:51.104Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"radius\",\"params\":{\"field\":\"bytes\",\"customLabel\":\"bubbles\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"f92e5630-623e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218933787,95],"type":"visualization","updated_at":"2021-03-31T19:28:53.787Z","version":"WzI5LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_heatmap","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0% - 25%\":\"rgb(255,255,204)\",\"25% - 50%\":\"rgb(254,217,118)\",\"50% - 75%\":\"rgb(253,141,60)\",\"75% - 100%\":\"rgb(227,27,28)\"}}}","version":1,"visState":"{\"title\":\"logstash_heatmap\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Yellow to Red\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":true,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}],\"row\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"response.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"9853d4d0-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218934821,97],"type":"visualization","updated_at":"2021-03-31T19:28:54.821Z","version":"WzMxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_goalchart","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 33\":\"rgb(0,104,55)\",\"33 - 67\":\"rgb(255,255,190)\",\"67 - 100\":\"rgb(165,0,38)\"}}}","version":1,"visState":"{\"title\":\"logstash_goalchart\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000},{\"from\":10001,\"to\":20000},{\"from\":20001,\"to\":30000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"group\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"},"coreMigrationVersion":"7.12.1","id":"6ecb33b0-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218935846,99],"type":"visualization","updated_at":"2021-03-31T19:28:55.846Z","version":"WzMzLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_gauge","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(0,104,55)\",\"50 - 75\":\"rgb(255,255,190)\",\"75 - 100\":\"rgb(165,0,38)\"}}}","version":1,"visState":"{\"title\":\"logstash_gauge\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true},\"alignment\":\"horizontal\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes\",\"ranges\":[{\"from\":0,\"to\":10001},{\"from\":10002,\"to\":1000000}],\"json\":\"\"}}]}"},"coreMigrationVersion":"7.12.1","id":"b8e35c80-623c-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218936874,101],"type":"visualization","updated_at":"2021-03-31T19:28:56.874Z","version":"WzM1LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_coordinatemaps","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_coordinatemaps\",\"type\":\"tile_map\",\"params\":{\"colorSchema\":\"Yellow to Red\",\"mapType\":\"Scaled Circle Markers\",\"isDesaturated\":false,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"selectedTmsLayer\":{\"origin\":\"elastic_maps_service\",\"id\":\"road_map\",\"minZoom\":0,\"maxZoom\":18,\"attribution\":\"

© OpenStreetMap contributors|OpenMapTiles|Elastic Maps Service

\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"geohash_grid\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.coordinates\",\"autoPrecision\":true,\"isFilteredByCollar\":true,\"useGeocentroid\":true,\"mapZoom\":2,\"mapCenter\":[0,0],\"precision\":2,\"customLabel\":\"logstash src/dest\"}}]}"},"coreMigrationVersion":"7.12.1","id":"f1bc75d0-6239-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218937901,31],"type":"visualization","updated_at":"2021-03-31T19:28:57.901Z","version":"WzM3LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"title":"logstash_inputcontrols","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_inputcontrols\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1611928563867\",\"fieldName\":\"machine.ram\",\"parent\":\"\",\"label\":\"Logstash RAM\",\"type\":\"range\",\"options\":{\"decimalPlaces\":0,\"step\":1024},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1611928586274\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"Logstash OS\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}"},"coreMigrationVersion":"7.12.1","id":"d79fe3d0-6239-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"control_0_index_pattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"control_1_index_pattern","type":"index-pattern"}],"sort":[1617218939955,36],"type":"visualization","updated_at":"2021-03-31T19:28:59.955Z","version":"WzQxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"title":"logstash_markdown","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_markdown\",\"type\":\"markdown\",\"params\":{\"fontSize\":12,\"openLinksInNewTab\":true,\"markdown\":\"Kibana is built with JS https://www.javascript.com/\"},\"aggs\":[]}"},"coreMigrationVersion":"7.12.1","id":"318375a0-6240-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[],"sort":[1617218940976,37],"type":"visualization","updated_at":"2021-03-31T19:29:00.976Z","version":"WzQzLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"title":"logstash_vegaviz","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_vegaviz\",\"type\":\"vega\",\"params\":{\"spec\":\"{\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v2.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: logstash-*\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 13\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"aggs\":[]}"},"coreMigrationVersion":"7.12.1","id":"e461eb20-6245-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[],"sort":[1617218942061,29],"type":"visualization","updated_at":"2021-03-31T19:29:02.061Z","version":"WzQ1LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_regionmap","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_regionmap\",\"type\":\"region_map\",\"params\":{\"addTooltip\":true,\"colorSchema\":\"Yellow to Red\",\"emsHotLink\":\"https://maps.elastic.co/v6.7?locale=en#file/world_countries\",\"isDisplayWarning\":true,\"legendPosition\":\"bottomright\",\"mapCenter\":[0,0],\"mapZoom\":2,\"outlineWeight\":1,\"selectedJoinField\":{\"type\":\"id\",\"name\":\"iso2\",\"description\":\"ISO 3166-1 alpha-2 code\"},\"showAllShapes\":true,\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"selectedTmsLayer\":{\"origin\":\"elastic_maps_service\",\"id\":\"road_map\",\"minZoom\":0,\"maxZoom\":18,\"attribution\":\"

© OpenStreetMap contributors|OpenMapTiles|Elastic Maps Service

\"}},\"selectedLayer\":{\"name\":\"World Countries\",\"origin\":\"elastic_maps_service\",\"id\":\"world_countries\",\"created_at\":\"2017-04-26T17:12:15.978370\",\"attribution\":\"Made with NaturalEarth | Elastic Maps Service\",\"fields\":[{\"type\":\"id\",\"name\":\"iso2\",\"description\":\"ISO 3166-1 alpha-2 code\"},{\"type\":\"id\",\"name\":\"iso3\",\"description\":\"ISO 3166-1 alpha-3 code\"},{\"type\":\"property\",\"name\":\"name\",\"description\":\"name\"}],\"format\":{\"type\":\"geojson\"},\"layerId\":\"elastic_maps_service.World Countries\",\"isEMS\":true}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.dest\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"25bdc750-6242-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218943039,44],"type":"visualization","updated_at":"2021-03-31T19:29:03.039Z","version":"WzQ3LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_verticalbarchart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_verticalbarchart\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\",\"defaultYExtents\":true},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true,\"row\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-09-18T06:38:43.311Z\",\"to\":\"2015-09-26T04:02:51.104Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"h\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{},\"scaleMetricValues\":true}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"response.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Response code\"}}]}"},"coreMigrationVersion":"7.12.1","id":"71dd7bc0-6248-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218944094,47],"type":"visualization","updated_at":"2021-03-31T19:29:04.094Z","version":"WzUxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_metricviz","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_metricviz\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes_scripted\",\"ranges\":[{\"from\":0,\"to\":10000},{\"from\":10001,\"to\":300000}]}}]}"},"coreMigrationVersion":"7.12.1","id":"6aea48a0-6240-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218946147,51],"type":"visualization","updated_at":"2021-03-31T19:29:06.147Z","version":"WzU1LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_piechart","uiStateJSON":"{}","version":1,"visState":"{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"machine.os.raw\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"labels\":{\"last_level\":true,\"show\":false,\"truncate\":100,\"values\":true},\"legendPosition\":\"right\",\"type\":\"pie\"},\"title\":\"logstash_piechart\",\"type\":\"pie\"}"},"coreMigrationVersion":"7.12.1","id":"32b681f0-6241-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218947175,107],"type":"visualization","updated_at":"2021-03-31T19:29:07.175Z","version":"WzU3LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_tagcloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_tagcloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"log\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.srcdest\",\"size\":23,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"ccca99e0-6244-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218948213,111],"type":"visualization","updated_at":"2021-03-31T19:29:08.213Z","version":"WzU5LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"title":"logstash_timelion","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_timelion\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(q='machine.os.raw:win xp' , index=logstash-*)\",\"interval\":\"auto\"},\"aggs\":[]}"},"coreMigrationVersion":"7.12.1","id":"a4d7be80-6245-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[],"sort":[1617218949236,113],"type":"visualization","updated_at":"2021-03-31T19:29:09.236Z","version":"WzYxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{}"},"title":"logstash_tsvb","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_tsvb\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"default_index_pattern\":\"logstash-*\",\"annotations\":[{\"fields\":\"machine.os.raw\",\"template\":\"{{machine.os.raw}}\",\"index_pattern\":\"logstash-*\",\"query_string\":{\"query\":\"machine.os.raw :\\\"win xp\\\" \",\"language\":\"lucene\"},\"id\":\"aa43ceb0-6248-11eb-9a82-ef1c6e6c0265\",\"color\":\"#F00\",\"time_field\":\"@timestamp\",\"icon\":\"fa-tag\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}]},\"aggs\":[]}"},"coreMigrationVersion":"7.12.1","id":"c94d8440-6248-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[],"sort":[1617218951289,112],"type":"visualization","updated_at":"2021-03-31T19:29:11.289Z","version":"WzY1LDFd"} +{"attributes":{"columns":["bytes_scripted"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"machine.os.raw :\\\"win xp\\\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"logstash_scripted_saved_search","version":1},"coreMigrationVersion":"7.12.1","id":"db6226f0-61c0-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218928794,16],"type":"search","updated_at":"2021-03-31T19:28:48.794Z","version":"WzE5LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"}]","timeRestore":false,"title":"logstash_dashboardwithtime","version":1},"coreMigrationVersion":"7.12.1","id":"154944b0-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"panel_0","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"panel_1","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"panel_2","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"panel_3","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"panel_4","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"panel_5","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"panel_6","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"panel_7","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"panel_8","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"panel_9","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"panel_10","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"panel_11","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"panel_12","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"panel_13","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"panel_14","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"panel_15","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"panel_16","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"panel_17","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"panel_18","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"panel_19","type":"search"}],"sort":[1617218953348,182],"type":"dashboard","updated_at":"2021-03-31T19:29:13.348Z","version":"WzY5LDFd"} +{"attributes":{"fieldAttrs":"{\"speaker\":{\"count\":1},\"text_entry\":{\"count\":6},\"type\":{\"count\":3}}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.12.1","id":"4e937b20-619d-11eb-aebf-c306684b328d","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1617218924067,3],"type":"index-pattern","updated_at":"2021-03-31T19:28:44.067Z","version":"WzcsMV0="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_areachart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_areachart\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"mode\":\"stacked\",\"type\":\"histogram\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"2\",\"label\":\"Count\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"2\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"play_name\",\"size\":20,\"order\":\"desc\",\"orderBy\":\"2\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"play_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"2\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"185283c0-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218945128,49],"type":"visualization","updated_at":"2021-03-31T19:29:05.128Z","version":"WzUzLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_piechart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_piechart\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":false,\"labels\":{\"show\":true,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"play_name\",\"size\":15,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"33736660-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218950263,109],"type":"visualization","updated_at":"2021-03-31T19:29:10.263Z","version":"WzYzLDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"}]","timeRestore":false,"title":"logstash_dashboard_withouttime","version":1},"coreMigrationVersion":"7.12.1","id":"5d3410c0-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"panel_0","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"panel_1","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"panel_2","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"panel_3","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"panel_4","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"panel_5","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"panel_6","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"panel_7","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"panel_8","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"panel_9","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"panel_10","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"panel_11","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"panel_12","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"panel_13","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"panel_14","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"panel_15","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"panel_16","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"panel_17","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"panel_18","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"panel_19","type":"search"}],"sort":[1617218954375,161],"type":"dashboard","updated_at":"2021-03-31T19:29:14.375Z","version":"WzcxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_tag_cloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_tag_cloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"multiple\",\"minFontSize\":59,\"maxFontSize\":100,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"type.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.12.1","id":"622ac7f0-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.12.0"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218929689,13],"type":"visualization","updated_at":"2021-03-31T19:28:49.689Z","version":"WzIyLDFd"} +{"attributes":{"buildNum":9007199254740991,"defaultIndex":"56b34100-619d-11eb-aebf-c306684b328d"},"coreMigrationVersion":"7.12.1","id":"7.12.1","migrationVersion":{"config":"7.12.0"},"references":[],"sort":[1617218966119,191],"type":"config","updated_at":"2021-03-31T19:29:26.119Z","version":"Wzc3LDFd"} +{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"text_entry\",\"value\":\"Christendom.\",\"params\":{\"query\":\"Christendom.\",\"type\":\"phrase\"},\"disabled\":false,\"alias\":null,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"text_entry\":{\"query\":\"Christendom.\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_search","version":1},"coreMigrationVersion":"7.12.1","id":"712ebbe0-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1617218925706,93],"type":"search","updated_at":"2021-03-31T19:28:45.706Z","version":"WzEzLDFd"} +{"attributes":{"columns":["play_name","speaker"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"speaker:\\\"GLOUCESTER\\\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_lucene_search","version":1},"coreMigrationVersion":"7.12.1","id":"ddacc820-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218927635,25],"type":"search","updated_at":"2021-03-31T19:28:47.635Z","version":"WzE2LDFd"} +{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"text_entry :\\\"MORDAKE THE EARL OF FIFE, AND ELDEST SON\\\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_kql_search","version":1},"coreMigrationVersion":"7.12.1","id":"f852d570-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1617218926603,23],"type":"search","updated_at":"2021-03-31T19:28:46.603Z","version":"WzE0LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]","timeRestore":false,"title":"shakespeare_dashboard","version":1},"coreMigrationVersion":"7.12.1","id":"73398a90-619e-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"185283c0-619e-11eb-aebf-c306684b328d","name":"panel_0","type":"visualization"},{"id":"33736660-619e-11eb-aebf-c306684b328d","name":"panel_1","type":"visualization"},{"id":"622ac7f0-619e-11eb-aebf-c306684b328d","name":"panel_2","type":"visualization"},{"id":"712ebbe0-619d-11eb-aebf-c306684b328d","name":"panel_3","type":"search"},{"id":"ddacc820-619d-11eb-aebf-c306684b328d","name":"panel_4","type":"search"},{"id":"f852d570-619d-11eb-aebf-c306684b328d","name":"panel_5","type":"search"}],"sort":[1617218931742,88],"type":"dashboard","updated_at":"2021-03-31T19:28:51.742Z","version":"WzI2LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.srcdest\",\"value\":\"IN:US\",\"params\":{\"query\":\"IN:US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.srcdest\":{\"query\":\"IN:US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"}]","timeRestore":false,"title":"logstash_dashboardwithfilters","version":1},"coreMigrationVersion":"7.12.1","id":"79794f20-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"panel_0","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"panel_1","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"panel_2","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"panel_3","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"panel_4","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"panel_5","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"panel_6","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"panel_7","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"panel_8","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"panel_9","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"panel_10","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"panel_11","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"panel_12","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"panel_13","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"panel_14","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"panel_15","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"panel_16","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"panel_17","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"panel_18","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"panel_19","type":"search"}],"sort":[1617218955401,140],"type":"dashboard","updated_at":"2021-03-31T19:29:15.401Z","version":"WzczLDFd"} +{"exportedCount":33,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts b/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts new file mode 100644 index 0000000000000..07fe0e910ea99 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* This test is importing saved objects from 7.12.0 to 8.0 and the backported version + * will import from 6.8.x to 8.0.0 + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('Export import saved objects between versions', function () { + beforeEach(async function () { + await esArchiver.load('logstash_functional'); + await esArchiver.load('getting_started/shakespeare'); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + + after(async () => { + await esArchiver.unload('logstash_functional'); + await esArchiver.unload('getting_started/shakespeare'); + await esArchiver.load('empty_kibana'); + }); + + it('should be able to import 7.12 saved objects into 8.0.0', async function () { + await retry.tryForTime(10000, async () => { + const existingSavedObjects = await testSubjects.getVisibleText('exportAllObjects'); + // Kibana always has 1 advanced setting as a saved object + await expect(existingSavedObjects).to.be('Export 1 object'); + }); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', '_7.12_import_saved_objects.ndjson') + ); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + const importedSavedObjects = await testSubjects.getVisibleText('exportAllObjects'); + // verifying the count of saved objects after importing .ndjson + await expect(importedSavedObjects).to.be('Export 34 objects'); + }); + }); +} diff --git a/x-pack/test/functional/apps/saved_objects_management/index.ts b/x-pack/test/functional/apps/saved_objects_management/index.ts index 602f87c1af38e..d474755af4676 100644 --- a/x-pack/test/functional/apps/saved_objects_management/index.ts +++ b/x-pack/test/functional/apps/saved_objects_management/index.ts @@ -13,5 +13,6 @@ export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderC loadTestFile(require.resolve('./spaces_integration')); loadTestFile(require.resolve('./feature_controls/saved_objects_management_security')); + loadTestFile(require.resolve('./import_saved_objects_between_versions')); }); } diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 51d460e386ebe..3e6ee3a2f8867 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -11,21 +11,22 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); + const find = getService('find'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'header']); const USERS_PATH = 'security/users'; const EDIT_USERS_PATH = `${USERS_PATH}/edit`; + const CREATE_USERS_PATH = `${USERS_PATH}/create`; const ROLES_PATH = 'security/roles'; const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`; const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`; + const security = getService('security'); - // FLAKY: https://github.com/elastic/kibana/issues/61173 - describe.skip('Management', function () { + describe('Management', function () { this.tags(['skipFirefox']); before(async () => { - // await PageObjects.security.login('elastic', 'changeme'); await PageObjects.security.initTests(); await kibanaServer.uiSettings.update({ defaultIndex: 'logstash-*', @@ -43,20 +44,26 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); }); + after(async () => { + await security.role.delete('logstash-readonly'); + await security.user.delete('dashuser', 'new-user'); + await PageObjects.security.forceLogout(); + }); + describe('Security', () => { describe('navigation', () => { it('Can navigate to create user section', async () => { await PageObjects.security.clickElasticsearchUsers(); await PageObjects.security.clickCreateNewUser(); const currentUrl = await browser.getCurrentUrl(); - expect(currentUrl).to.contain(EDIT_USERS_PATH); + expect(currentUrl).to.contain(CREATE_USERS_PATH); }); it('Clicking cancel in create user section brings user back to listing', async () => { await PageObjects.security.clickCancelEditUser(); const currentUrl = await browser.getCurrentUrl(); expect(currentUrl).to.contain(USERS_PATH); - expect(currentUrl).to.not.contain(EDIT_USERS_PATH); + expect(currentUrl).to.not.contain(CREATE_USERS_PATH); }); it('Clicking save in create user section brings user back to listing', async () => { @@ -67,12 +74,11 @@ export default function ({ getService, getPageObjects }) { await testSubjects.setValue('passwordConfirmationInput', '123456'); await testSubjects.setValue('userFormFullNameInput', 'Full User Name'); await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.clickSaveCreateUser(); const currentUrl = await browser.getCurrentUrl(); expect(currentUrl).to.contain(USERS_PATH); - expect(currentUrl).to.not.contain(EDIT_USERS_PATH); + expect(currentUrl).to.not.contain(CREATE_USERS_PATH); }); it('Can navigate to edit user section', async () => { @@ -143,14 +149,11 @@ export default function ({ getService, getPageObjects }) { await testSubjects.setValue('passwordConfirmationInput', '123456'); await testSubjects.setValue('userFormFullNameInput', 'dashuser'); await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); await PageObjects.security.assignRoleToUser('logstash-readonly'); - - await PageObjects.security.clickSaveEditUser(); - + await PageObjects.security.clickSaveCreateUser(); await PageObjects.settings.navigateTo(); await testSubjects.click('users'); - await PageObjects.settings.clickLinkText('kibana_dashboard_only_user'); + await find.clickByButtonText('logstash-readonly'); const currentUrl = await browser.getCurrentUrl(); expect(currentUrl).to.contain(EDIT_ROLES_PATH); }); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 0cab12bc6672f..8730ee3aeeaf2 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -90,7 +90,7 @@ export default function ({ getService, getPageObjects }) { expect(roles.apm_system.deprecated).to.be(false); expect(roles.apm_user.reserved).to.be(true); - expect(roles.apm_user.deprecated).to.be(false); + expect(roles.apm_user.deprecated).to.be(true); expect(roles.beats_admin.reserved).to.be(true); expect(roles.beats_admin.deprecated).to.be(false); diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index 955618774bdfd..b72656a96980f 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'snapshotRestore']); const log = getService('log'); - const es = getService('legacyEs'); + const es = getService('es'); describe('Home page', function () { before(async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index c0323d96026ef..177a2cf719dd0 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -86,7 +86,7 @@ export default async function ({ readConfigFile }) { '--xpack.maps.enableDrawingFeature=true', '--xpack.reporting.queue.pollInterval=3000', // make it explicitly the default '--xpack.reporting.csv.maxSizeBytes=2850', // small-ish limit for cutting off a 1999 byte report - '--stats.maximumWaitTimeForAllCollectorsInS=1', + '--usageCollection.maximumWaitTimeForAllCollectorsInS=1', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--timelion.ui.enabled=true', diff --git a/x-pack/test/functional/es_archives/banners/multispace/data.json b/x-pack/test/functional/es_archives/banners/multispace/data.json new file mode 100644 index 0000000000000..fc0e0dc7b7eee --- /dev/null +++ b/x-pack/test/functional/es_archives/banners/multispace/data.json @@ -0,0 +1,62 @@ +{ + "type": "doc", + "value": { + "id": "config:6.0.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "http://example.com/evil" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "id": "another-space:config:6.0.0", + "index": ".kibana", + "source": { + "namespace": "another-space", + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "/app/canvas" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "description": "This is the default space!", + "name": "Default" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:another-space", + "index": ".kibana", + "source": { + "space": { + "description": "This is another space", + "name": "Another Space" + }, + "type": "space" + } + } +} diff --git a/x-pack/test/functional/es_archives/banners/multispace/mappings.json b/x-pack/test/functional/es_archives/banners/multispace/mappings.json new file mode 100644 index 0000000000000..f3793c7ca6780 --- /dev/null +++ b/x-pack/test/functional/es_archives/banners/multispace/mappings.json @@ -0,0 +1,287 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultRoute": { + "type": "keyword" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/getting_started/shakespeare/data.json.gz b/x-pack/test/functional/es_archives/getting_started/shakespeare/data.json.gz new file mode 100644 index 0000000000000..dcd31ef31085e Binary files /dev/null and b/x-pack/test/functional/es_archives/getting_started/shakespeare/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/getting_started/shakespeare/mappings.json b/x-pack/test/functional/es_archives/getting_started/shakespeare/mappings.json new file mode 100644 index 0000000000000..0e030c54d4912 --- /dev/null +++ b/x-pack/test/functional/es_archives/getting_started/shakespeare/mappings.json @@ -0,0 +1,28 @@ +{ + "type": "index", + "value": { + "index": "shakespeare", + "mappings": { + "properties": { + "line_id": { + "type": "integer" + }, + "play_name": { + "type": "keyword" + }, + "speaker": { + "type": "keyword" + }, + "speech_number": { + "type": "integer" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "5" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/banners_page.ts b/x-pack/test/functional/page_objects/banners_page.ts new file mode 100644 index 0000000000000..d2e4e43cec117 --- /dev/null +++ b/x-pack/test/functional/page_objects/banners_page.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function BannersPageProvider({ getService }: FtrProviderContext) { + const find = getService('find'); + + class BannersPage { + isTopBannerVisible() { + return find.existsByCssSelector('.header__topBanner .kbnUserBanner__container'); + } + + async getTopBannerText() { + if (!(await this.isTopBannerVisible())) { + return ''; + } + const bannerContainer = await find.byCssSelector( + '.header__topBanner .kbnUserBanner__container' + ); + return bannerContainer.getVisibleText(); + } + } + + return new BannersPage(); +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index cf92191075fba..81c0328e76342 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -41,6 +41,7 @@ import { TagManagementPageProvider } from './tag_management_page'; import { NavigationalSearchProvider } from './navigational_search'; import { SearchSessionsPageProvider } from './search_sessions_management_page'; import { DetectionsPageProvider } from '../../security_solution_ftr/page_objects/detections'; +import { BannersPageProvider } from './banners_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -78,5 +79,6 @@ export const pageObjects = { roleMappings: RoleMappingsPageProvider, ingestPipelines: IngestPipelinesPageProvider, navigationalSearch: NavigationalSearchProvider, + banners: BannersPageProvider, detections: DetectionsPageProvider, }; diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 205a4391062a2..65020be390f9d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -197,7 +197,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async searchField(name: string) { - await testSubjects.setValue('lnsIndexPatternFieldSearch', name); + await testSubjects.setValue('lnsIndexPatternFieldSearch', name, { + clearWithKeyboard: true, + typeCharByChar: true, + }); }, async waitForField(field: string) { diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 3c0ef7348a4b3..97a5c517db794 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -5,6 +5,7 @@ * 2.0. */ +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../ftr_provider_context'; import { AuthenticatedUser, Role } from '../../../plugins/security/common/model'; @@ -289,6 +290,11 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } + async clickSaveCreateUser() { + await find.clickByButtonText('Create user'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + async clickSaveEditRole() { const saveButton = await retry.try(() => testSubjects.find('roleFormSaveButton')); await saveButton.moveMouseTo(); diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts index 81870a948dc15..42e7771b14401 100644 --- a/x-pack/test/functional_cors/config.ts +++ b/x-pack/test/functional_cors/config.ts @@ -8,6 +8,7 @@ import Url from 'url'; import Path from 'path'; import type { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { kbnTestConfig } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts index e6c3f4b05aabd..e128ec6f13e77 100644 --- a/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts +++ b/x-pack/test/functional_cors/plugins/kibana_cors_test/server/plugin.ts @@ -6,6 +6,7 @@ */ import Hapi from '@hapi/hapi'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { kbnTestConfig } from '@kbn/test'; import { take } from 'rxjs/operators'; import Url from 'url'; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 550e6ca455b22..7b760dfb8b6a1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -54,7 +54,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('rulesTab'); } - // Failing: See https://github.com/elastic/kibana/issues/95590 + // FLAKY: https://github.com/elastic/kibana/issues/95591 describe.skip('alerts list', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); @@ -129,13 +129,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const disableSwitchAfterDisable = await testSubjects.find('disableSwitch'); - const isChecked = await disableSwitchAfterDisable.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'disableSwitch', + 'true' + ); }); it('should re-enable single alert', async () => { @@ -147,19 +145,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'disableSwitch', + 'true' + ); + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const disableSwitchAfterReEnable = await testSubjects.find('disableSwitch'); - const isChecked = await disableSwitchAfterReEnable.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'disableSwitch', + 'false' + ); }); it('should mute single alert', async () => { @@ -171,13 +173,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const muteSwitchAfterMute = await testSubjects.find('muteSwitch'); - const isChecked = await muteSwitchAfterMute.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'muteSwitch', + 'true' + ); }); it('should unmute single alert', async () => { @@ -189,19 +189,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'muteSwitch', + 'true' + ); + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const muteSwitchAfterUnmute = await testSubjects.find('muteSwitch'); - const isChecked = await muteSwitchAfterUnmute.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'muteSwitch', + 'false' + ); }); it('should delete single alert', async () => { diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 8d4311a3ec322..e5971ddba415f 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -186,5 +186,18 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) expect(isConfirmationModalVisible).to.eql(true, 'Expect confirmation modal to be visible'); await testSubjects.click('confirmModalConfirmButton'); }, + async ensureRuleActionToggleApplied( + ruleName: string, + switchName: string, + shouldBeCheckedAsString: string + ) { + await retry.try(async () => { + await this.searchAlerts(ruleName); + await testSubjects.click('collapsedItemActions'); + const switchControl = await testSubjects.find(switchName); + const isChecked = await switchControl.getAttribute('aria-checked'); + expect(isChecked).to.eql(shouldBeCheckedAsString); + }); + }, }; } diff --git a/x-pack/test/licensing_plugin/config.public.ts b/x-pack/test/licensing_plugin/config.public.ts index 35f3bfa11fea0..0de536d7125ca 100644 --- a/x-pack/test/licensing_plugin/config.public.ts +++ b/x-pack/test/licensing_plugin/config.public.ts @@ -6,6 +6,7 @@ */ import path from 'path'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { KIBANA_ROOT } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 9261650bb5d4e..104d11eb87f7c 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -7,6 +7,7 @@ import { resolve } from 'path'; import fs from 'fs'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { KIBANA_ROOT } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; @@ -29,6 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './test_suites/resolver'), resolve(__dirname, './test_suites/global_search'), + resolve(__dirname, './test_suites/timelines'), ], services, @@ -46,6 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { KIBANA_ROOT, 'test/plugin_functional/plugins/core_provider_plugin' )}`, + '--xpack.timelines.enabled=true', ...plugins.map((pluginDir) => `--plugin-path=${resolve(__dirname, 'plugins', pluginDir)}`), ], }, @@ -59,6 +62,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { resolverTest: { pathname: '/app/resolverTest', }, + timelineTest: { + pathname: '/app/timelinesTest', + }, }, // choose where esArchiver should load archives from diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json new file mode 100644 index 0000000000000..85c2639ef7d47 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json @@ -0,0 +1,12 @@ +{ + "id": "timelinesTest", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "timelinesTest"], + "requiredPlugins": ["timelines"], + "requiredBundles": [ + "kibanaReact" + ], + "server": false, + "ui": true +} diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx new file mode 100644 index 0000000000000..a6772c3b0bb5b --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.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 { Router } from 'react-router-dom'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TimelinesPluginSetup } from '../../../../../../../plugins/timelines/public'; + +/** + * Render the Timeline Test app. Returns a cleanup function. + */ +export function renderApp( + coreStart: CoreStart, + parameters: AppMountParameters, + timelinesPluginSetup: TimelinesPluginSetup +) { + ReactDOM.render( + , + parameters.element + ); + + return () => { + ReactDOM.unmountComponentAtNode(parameters.element); + }; +} + +const AppRoot = React.memo( + ({ + coreStart, + parameters, + timelinesPluginSetup, + }: { + coreStart: CoreStart; + parameters: AppMountParameters; + timelinesPluginSetup: TimelinesPluginSetup; + }) => { + return ( + + + + {(timelinesPluginSetup.getTimeline && + timelinesPluginSetup.getTimeline({ timelineId: 'test' })) ?? + null} + + + + ); + } +); diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/index.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/index.ts new file mode 100644 index 0000000000000..5f038b5b933e6 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer } from 'kibana/public'; +import { + TimelinesTestPlugin, + TimelinesTestPluginSetupDependencies, + TimelinesTestPluginStartDependencies, +} from './plugin'; + +export const plugin: PluginInitializer< + void, + void, + TimelinesTestPluginSetupDependencies, + TimelinesTestPluginStartDependencies +> = () => new TimelinesTestPlugin(); diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts new file mode 100644 index 0000000000000..5cf900e194d0c --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.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 { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { TimelinesPluginSetup } from '../../../../../plugins/timelines/public'; +import { renderApp } from './applications/timelines_test'; + +export type TimelinesTestPluginSetup = void; +export type TimelinesTestPluginStart = void; +export interface TimelinesTestPluginSetupDependencies { + timelines: TimelinesPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TimelinesTestPluginStartDependencies {} + +export class TimelinesTestPlugin + implements + Plugin< + TimelinesTestPluginSetup, + void, + TimelinesTestPluginSetupDependencies, + TimelinesTestPluginStartDependencies + > { + public setup( + core: CoreSetup, + setupDependencies: TimelinesTestPluginSetupDependencies + ) { + core.application.register({ + id: 'timelinesTest', + title: i18n.translate('xpack.timelinesTest.pluginTitle', { + defaultMessage: 'Timelines Test', + }), + mount: async (params: AppMountParameters) => { + const startServices = await core.getStartServices(); + const [coreStart] = startServices; + const { timelines } = setupDependencies; + + return renderApp(coreStart, params, timelines); + }, + }); + } + + public start() {} +} diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index 077044d29f7d9..a44ded43a0bfe 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('TOTO GlobalSearchBar', function () { + describe('GlobalSearchBar', function () { const { common, navigationalSearch } = getPageObjects(['common', 'navigationalSearch']); const esArchiver = getService('esArchiver'); const browser = getService('browser'); diff --git a/x-pack/test/plugin_functional/test_suites/timelines/index.ts b/x-pack/test/plugin_functional/test_suites/timelines/index.ts new file mode 100644 index 0000000000000..655ed9dc3898a --- /dev/null +++ b/x-pack/test/plugin_functional/test_suites/timelines/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + describe('Timelines plugin API', function () { + this.tags('ciGroup7'); + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + describe('timelines plugin rendering', function () { + before(async () => { + await pageObjects.common.navigateToApp('timelineTest'); + }); + it('shows the timeline component on navigation', async () => { + await testSubjects.existOrFail('timeline-wrapper'); + }); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts index 6627cb3be5ed5..ddd6fe046dd31 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts @@ -5,6 +5,7 @@ * 2.0. */ +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { esTestConfig, kbnTestConfig, kibanaServerTestUser } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { format as formatUrl } from 'url'; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap index c7ef39f65f552..094d72942353d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap +++ b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap @@ -8,28 +8,28 @@ exports[`Reporting APIs CSV Generation from SearchSource Exports CSV with all fi 24.5 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan 9gMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",EUR,Pia,Pia,\\"Pia Richards\\",\\"Pia Richards\\",FEMALE,45,Richards,Richards,,Saturday,5,\\"pia@richards-family.zzz\\",Cannes,Europe,FR,\\"{ \\"\\"coordinates\\"\\": [ 7, 43.6 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Alpes-Maritimes\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591503,\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"20.99, 20.99\\",\\"20.99, 20.99\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"10.7, 9.87\\",\\"20.99, 20.99\\",\\"14,761, 11,632\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"1, 1\\",\\"ZO0006400064, ZO0150601506\\",\\"0, 0\\",\\"20.99, 20.99\\",\\"20.99, 20.99\\",\\"0, 0\\",\\"ZO0006400064, ZO0150601506\\",\\"41.98\\",\\"41.98\\",2,2,order,pia +}\\",\\"Alpes-Maritimes\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591503,\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"20.984, 20.984\\",\\"20.984, 20.984\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"10.703, 9.867\\",\\"20.984, 20.984\\",\\"14,761, 11,632\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"1, 1\\",\\"ZO0006400064, ZO0150601506\\",\\"0, 0\\",\\"20.984, 20.984\\",\\"20.984, 20.984\\",\\"0, 0\\",\\"ZO0006400064, ZO0150601506\\",\\"41.969\\",\\"41.969\\",2,2,order,pia BgMtOW0BH63Xcmy432LJ,ecommerce,\\"-\\",\\"-\\",\\"Women's Clothing\\",\\"Women's Clothing\\",EUR,Brigitte,Brigitte,\\"Brigitte Meyer\\",\\"Brigitte Meyer\\",FEMALE,12,Meyer,Meyer,,Saturday,5,\\"brigitte@meyer-family.zzz\\",\\"New York\\",\\"North America\\",US,\\"{ \\"\\"coordinates\\"\\": [ -74, 40.8 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"New York\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591709,\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"7.99, 32.99\\",\\"7.99, 32.99\\",\\"Women's Clothing, Women's Clothing\\",\\"Women's Clothing, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"3.6, 17.48\\",\\"7.99, 32.99\\",\\"20,734, 7,539\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"1, 1\\",\\"ZO0638206382, ZO0038800388\\",\\"0, 0\\",\\"7.99, 32.99\\",\\"7.99, 32.99\\",\\"0, 0\\",\\"ZO0638206382, ZO0038800388\\",\\"40.98\\",\\"40.98\\",2,2,order,brigitte +}\\",\\"New York\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591709,\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"7.988, 33\\",\\"7.988, 33\\",\\"Women's Clothing, Women's Clothing\\",\\"Women's Clothing, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"3.6, 17.484\\",\\"7.988, 33\\",\\"20,734, 7,539\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"1, 1\\",\\"ZO0638206382, ZO0038800388\\",\\"0, 0\\",\\"7.988, 33\\",\\"7.988, 33\\",\\"0, 0\\",\\"ZO0638206382, ZO0038800388\\",\\"40.969\\",\\"40.969\\",2,2,order,brigitte KQMtOW0BH63Xcmy432LJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Clothing\\",\\"Men's Clothing\\",EUR,Abd,Abd,\\"Abd Mccarthy\\",\\"Abd Mccarthy\\",MALE,52,Mccarthy,Mccarthy,,Saturday,5,\\"abd@mccarthy-family.zzz\\",Cairo,Africa,EG,\\"{ \\"\\"coordinates\\"\\": [ 31.3, 30.1 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Cairo Governorate\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"Jul 12, 2019 @ 00:00:00.000\\",590937,\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"28.99, 12.99\\",\\"28.99, 12.99\\",\\"Men's Clothing, Men's Clothing\\",\\"Men's Clothing, Men's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"13.34, 6.11\\",\\"28.99, 12.99\\",\\"14,438, 23,607\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"1, 1\\",\\"ZO0297602976, ZO0565605656\\",\\"0, 0\\",\\"28.99, 12.99\\",\\"28.99, 12.99\\",\\"0, 0\\",\\"ZO0297602976, ZO0565605656\\",\\"41.98\\",\\"41.98\\",2,2,order,abd +}\\",\\"Cairo Governorate\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"Jul 12, 2019 @ 00:00:00.000\\",590937,\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"28.984, 12.992\\",\\"28.984, 12.992\\",\\"Men's Clothing, Men's Clothing\\",\\"Men's Clothing, Men's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"13.344, 6.109\\",\\"28.984, 12.992\\",\\"14,438, 23,607\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"1, 1\\",\\"ZO0297602976, ZO0565605656\\",\\"0, 0\\",\\"28.984, 12.992\\",\\"28.984, 12.992\\",\\"0, 0\\",\\"ZO0297602976, ZO0565605656\\",\\"41.969\\",\\"41.969\\",2,2,order,abd " `; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts index ffaa4cb2f8fb6..ebc7badd88f42 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts @@ -31,7 +31,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; - // Failing: See https://github.com/elastic/kibana/issues/95594 + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 describe.skip('CSV Generation from SearchSource', () => { before(async () => { await kibanaServer.uiSettings.update({ diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index 59d6074d9d8ca..20f9ff1b10592 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -5,6 +5,7 @@ * 2.0. */ +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { esTestConfig, kbnTestConfig } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { format as formatUrl } from 'url'; diff --git a/x-pack/test/security_api_integration/tests/anonymous/login.ts b/x-pack/test/security_api_integration/tests/anonymous/login.ts index 183016316fcdb..3d1a05583e904 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/login.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/login.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import request, { Cookie } from 'request'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts index 2fe88dc21e5e0..3deb1408dc5c9 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/kerberos_login.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import request, { Cookie } from 'request'; import { delay } from 'bluebird'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; import { diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts index b2dd65b4f2009..c13ce902ad658 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/oidc_auth.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import request, { Cookie } from 'request'; import url from 'url'; import { delay } from 'bluebird'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import { getStateAndNonce } from '../../../fixtures/oidc/oidc_tools'; import { FtrProviderContext } from '../../../ftr_provider_context'; diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index e3f63aad9e255..6eca9c354e248 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -11,6 +11,7 @@ import { delay } from 'bluebird'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/security_api_integration/tests/saml/saml_login.ts b/x-pack/test/security_api_integration/tests/saml/saml_login.ts index e199ba99bfc58..0a76628418c5e 100644 --- a/x-pack/test/security_api_integration/tests/saml/saml_login.ts +++ b/x-pack/test/security_api_integration/tests/saml/saml_login.ts @@ -10,6 +10,7 @@ import url from 'url'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; import request, { Cookie } from 'request'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import { getLogoutRequest, diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts index bb46beef41449..89bb79a4761a0 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts @@ -8,6 +8,7 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts index 60605c88ce45e..db41aca86e0ba 100644 --- a/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts +++ b/x-pack/test/security_api_integration/tests/session_invalidate/invalidate.ts @@ -7,6 +7,7 @@ import request, { Cookie } from 'request'; import expect from '@kbn/expect'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts index 0b17f037dfbd9..d2419ca07a434 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/cleanup.ts @@ -8,6 +8,7 @@ import request, { Cookie } from 'request'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { adminTestUser } from '@kbn/test'; import type { AuthenticationProvider } from '../../../../plugins/security/common/model'; import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools'; diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts index 646e0536c42f1..d91a772ccafac 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts @@ -7,6 +7,7 @@ import fs from 'fs'; import Path from 'path'; +// @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { KIBANA_ROOT } from '@kbn/test'; import { FtrProviderContext } from '../ftr_provider_context'; diff --git a/yarn.lock b/yarn.lock index 0bbfe98f5d1d8..8e2edde567ff2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,6 +1197,16 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.14.0.tgz#86fa0002bed2ce1123b7ad98d4dd4623a0d93244" integrity sha512-s0gyec6lArcRDwVfIP6xpY8iEaFpzrSpyErSppd3r2O49pOEg7n6HGS/qJ8ncvme56vrDk6crl/kQ6VAdEO+rg== +"@bazel/typescript@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-3.2.3.tgz#6e40bdb7c5294e588bac3b7d1269e58b98a1856c" + integrity sha512-Q1Yin/AYdh9yrkSJo3H6nVn6mMaohr5syjLd0Df0w7WI4zerdJTxrY5nhoWZwO/S1rPj8/MedDwZudCqPDeDMA== + dependencies: + protobufjs "6.8.8" + semver "5.6.0" + source-map-support "0.5.9" + tsutils "2.27.2" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1349,10 +1359,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@26.0.0": - version "26.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-26.0.0.tgz#42f06d3be0f40e0128e301b37bdfc509169c387b" - integrity sha512-5eBPSjdBb+pVDCcQOYA0dFBiYonHcw7ewxOUxgR8qMmay0xHc7gGUXZiDfIkyUDpJA+a9DS9juNNqKn/M4UbiQ== +"@elastic/charts@27.0.0": + version "27.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-27.0.0.tgz#cc6ea80dc90d07cfad0a932200cad2f6b217f7b8" + integrity sha512-gnLT+htGgcYzPUpa3NTBQyD8bw7t+0aAxdpVnBL7fZ0TdbX0xQ7u1yPEI9ljMbGguiVJMKoI1KMVLI49E3f1bg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1375,7 +1385,7 @@ utility-types "^3.10.0" uuid "^3.3.2" -"@elastic/datemath@link:packages/elastic-datemath": +"@elastic/datemath@link:bazel-bin/packages/elastic-datemath/npm_module": version "0.0.0" uid "" @@ -3441,6 +3451,59 @@ dependencies: "@babel/runtime" "^7.0.0" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + "@reach/router@^1.3.3": version "1.3.4" resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c" @@ -4814,14 +4877,6 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/globby@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@types/globby/-/globby-8.0.0.tgz#7bd10eaf802e1e11afdb1e5436cf472ddf4c0dd2" - integrity sha512-xDtsX5tlctxJzvg29r/LN12z30oJpoFP9cE8eJ8nY5cbSvN0c0RdRHrVlEq4LRh362Sd+JsqxJ3QWw0Wnyto8w== - dependencies: - "@types/glob" "*" - fast-glob "^2.0.2" - "@types/graceful-fs@*", "@types/graceful-fs@^4.1.2": version "4.1.3" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" @@ -5181,6 +5236,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== +"@types/long@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/lru-cache@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" @@ -5330,7 +5390,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@14.14.14", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^12.0.2": +"@types/node@*", "@types/node@14.14.14", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^12.0.2": version "14.14.14" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== @@ -9626,7 +9686,7 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.0, cliui@^7.0.2: +cliui@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.2.tgz#e3a412e1d5ec0ccbe50d1b4120fc8164e97881f4" integrity sha512-lhpKkuUj67j5JgZIPZxLe7nSa4MQoojzRVWQyzMqBp2hBg6gwRjUDAwC1YDeBaC3APDBKNnjWbv2mlDF4XgOSA== @@ -10044,15 +10104,14 @@ concat-stream@~2.0.0: typedarray "^0.0.6" concaveman@*: - version "1.1.1" - resolved "https://registry.yarnpkg.com/concaveman/-/concaveman-1.1.1.tgz#6c2482580b2523cef82fc2bec00a0415e6e68162" - integrity sha1-bCSCWAslI874L8K+wAoEFebmgWI= + version "1.2.0" + resolved "https://registry.yarnpkg.com/concaveman/-/concaveman-1.2.0.tgz#4340f27c08a11bdc1d5fac13476862a2ab09b703" + integrity sha512-OcqechF2/kubbffomKqjGEkb0ndlYhEbmyg/fxIGqdfYp5AZjD2Kl5hc97Hh3ngEuHU2314Z4KDbxL7qXGWrQQ== dependencies: - monotone-convex-hull-2d "^1.0.1" point-in-polygon "^1.0.1" - rbush "^2.0.1" - robust-orientation "^1.1.3" - tinyqueue "^1.1.0" + rbush "^3.0.0" + robust-predicates "^2.0.4" + tinyqueue "^2.0.3" config-chain@^1.1.12: version "1.1.12" @@ -11893,14 +11952,6 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" - integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== - dependencies: - arrify "^1.0.1" - path-type "^3.0.0" - dir-glob@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" @@ -13561,7 +13612,7 @@ fast-equals@^2.0.0: resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.0.tgz#bef2c423af3939f2c54310df54c57e64cd2adefc" integrity sha512-u6RBd8cSiLLxAiC04wVsLV6GBFDOXcTCgWkd3wEoFXgidPSoAJENqC9m7Jb2vewSvjBIfXV6icKeh3GTKfIaXA== -fast-glob@2.2.7, fast-glob@^2.0.2, fast-glob@^2.2.6: +fast-glob@2.2.7, fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== @@ -13573,22 +13624,10 @@ fast-glob@2.2.7, fast-glob@^2.0.2, fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" - integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fast-glob@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== +fast-glob@3.2.5, fast-glob@^3.0.3, fast-glob@^3.1.1, fast-glob@^3.2.2, fast-glob@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -14824,7 +14863,7 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.1, globby@^11.0.1: +globby@11.0.1: version "11.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== @@ -14850,6 +14889,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.1, globby@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -14861,19 +14912,6 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" - integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w== - dependencies: - array-union "^1.0.1" - dir-glob "2.0.0" - fast-glob "^2.0.2" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - globby@^9.2.0: version "9.2.0" resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" @@ -16183,11 +16221,6 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore@^3.3.5: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== - ignore@^4.0.3, ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -18238,10 +18271,10 @@ json-stringify-pretty-compact@1.2.0: resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-1.2.0.tgz#0bc316b5e6831c07041fc35612487fb4e9ab98b8" integrity sha512-/11Pj1OyX814QMKO7K8l85SHPTr/KsFxHp8GE2zVa0BtJgGimDjXHfM3FhC7keQdWDea7+nXf+f1de7ATZcZkQ== -json-stringify-pretty-compact@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885" - integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ== +json-stringify-pretty-compact@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz#f71ef9d82ef16483a407869556588e91b681d9ab" + integrity sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA== json-stringify-safe@5.0.1, json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" @@ -20241,13 +20274,6 @@ monocle-ts@^1.0.0: resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" integrity sha512-X9OzpOyd/R83sYex8NYpJjUzi/MLQMvGNVfxDYiIvs+QMXMEUDwR61MQoARFN10Cqz5h/mbFSPnIQNUIGhYd2Q== -monotone-convex-hull-2d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz#47f5daeadf3c4afd37764baa1aa8787a40eee08c" - integrity sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw= - dependencies: - robust-orientation "^1.1.3" - moo@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" @@ -22923,6 +22949,25 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= +protobufjs@6.8.8: + version "6.8.8" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" + integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.0" + "@types/node" "^10.1.0" + long "^4.0.0" + protocol-buffers-schema@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz#00434f608b4e8df54c59e070efeefc37fb4bb859" @@ -23143,11 +23188,6 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -quickselect@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2" - integrity sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ== - quickselect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" @@ -23258,14 +23298,7 @@ raw-loader@^4.0.1: loader-utils "^2.0.0" schema-utils "^2.6.5" -rbush@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/rbush/-/rbush-2.0.2.tgz#bb6005c2731b7ba1d5a9a035772927d16a614605" - integrity sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA== - dependencies: - quickselect "^1.0.1" - -rbush@^3.0.1: +rbush@^3.0.0, rbush@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== @@ -25131,33 +25164,10 @@ rison-node@1.0.2: resolved "https://registry.yarnpkg.com/rison-node/-/rison-node-1.0.2.tgz#b7b5f37f39f5ae2a51a973a33c9aa17239a33e4b" integrity sha1-t7Xzfzn1ripRqXOjPJqhcjmjPks= -robust-orientation@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/robust-orientation/-/robust-orientation-1.1.3.tgz#daff5b00d3be4e60722f0e9c0156ef967f1c2049" - integrity sha1-2v9bANO+TmByLw6cAVbvln8cIEk= - dependencies: - robust-scale "^1.0.2" - robust-subtract "^1.0.0" - robust-sum "^1.0.0" - two-product "^1.0.2" - -robust-scale@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/robust-scale/-/robust-scale-1.0.2.tgz#775132ed09542d028e58b2cc79c06290bcf78c32" - integrity sha1-d1Ey7QlULQKOWLLMecBikLz3jDI= - dependencies: - two-product "^1.0.2" - two-sum "^1.0.0" - -robust-subtract@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-subtract/-/robust-subtract-1.0.0.tgz#e0b164e1ed8ba4e3a5dda45a12038348dbed3e9a" - integrity sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo= - -robust-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/robust-sum/-/robust-sum-1.0.0.tgz#16646e525292b4d25d82757a286955e0bbfa53d9" - integrity sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k= +robust-predicates@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-2.0.4.tgz#0a2367a93abd99676d075981707f29cfb402248b" + integrity sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg== rollup@^0.25.8: version "0.25.8" @@ -25480,6 +25490,11 @@ semver-greatest-satisfied-range@^1.1.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -25797,11 +25812,6 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -25967,6 +25977,14 @@ source-map-resolve@^0.6.0: atob "^2.1.2" decode-uri-component "^0.2.0" +source-map-support@0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" + integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.3.2: version "0.3.3" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.3.3.tgz#34900977d5ba3f07c7757ee72e73bb1a9b53754f" @@ -27512,11 +27530,6 @@ tinygradient@0.4.3: "@types/tinycolor2" "^1.4.0" tinycolor2 "^1.0.0" -tinyqueue@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-1.2.3.tgz#b6a61de23060584da29f82362e45df1ec7353f3d" - integrity sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA== - tinyqueue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" @@ -27897,11 +27910,23 @@ tslib@^2.0.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== -tslib@^2.0.1, tslib@~2.0.3: +tslib@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== +tslib@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + +tsutils@2.27.2: + version "2.27.2" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7" + integrity sha512-qf6rmT84TFMuxAKez2pIfR8UCai49iQsfB7YWVjV1bKpy/d0PWT5rEOSM6La9PiHZ0k1RRZQiwVdVJfQ3BPHgg== + dependencies: + tslib "^1.8.1" + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -27931,16 +27956,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -two-product@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/two-product/-/two-product-1.0.2.tgz#67d95d4b257a921e2cb4bd7af9511f9088522eaa" - integrity sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo= - -two-sum@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/two-sum/-/two-sum-1.0.0.tgz#31d3f32239e4f731eca9df9155e2b297f008ab64" - integrity sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q= - type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -28974,13 +28989,6 @@ vega-expression@^4.0.1, vega-expression@~4.0.1: dependencies: vega-util "^1.16.0" -vega-expression@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-3.0.0.tgz#39179d010b34c57513162bf1ab5a7bff4b31be91" - integrity sha512-/ObjIOK94MB+ziTuh8HZt2eWlKUPT/piRJLal5tx5QL1sQbfRi++7lHKTaKMLXLqc4Xqp9/DewE3PqQ6tYzaUA== - dependencies: - vega-util "^1.15.2" - vega-force@~4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.0.7.tgz#6dc39ecb7889d9102661244d62fbc8d8714162ee" @@ -29051,10 +29059,10 @@ vega-label@~1.0.0: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-lite@^4.17.0: - version "4.17.0" - resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-4.17.0.tgz#01ad4535e92f28c3852c1071711de272ddfb4631" - integrity sha512-MO2XsaVZqx6iWWmVA5vwYFamvhRUsKfVp7n0pNlkZ2/21cuxelSl92EePZ2YGmzL6z4/3K7r/45zaG8p+qNHeg== +vega-lite@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.0.0.tgz#93898a910702736da41048f590882b907d78ac65" + integrity sha512-CrMAy3D2E662qtShrOeGttwwthRxUOZUfdu39THyxkOfLNJBCLkNjfQpFekEidxwbtFTO1zMZzyFIP3AE2I8kQ== dependencies: "@types/clone" "~2.1.0" "@types/fast-json-stable-stringify" "^2.0.0" @@ -29062,12 +29070,12 @@ vega-lite@^4.17.0: clone "~2.1.2" fast-deep-equal "~3.1.3" fast-json-stable-stringify "~2.1.0" - json-stringify-pretty-compact "~2.0.0" - tslib "~2.0.3" + json-stringify-pretty-compact "~3.0.0" + tslib "~2.1.0" vega-event-selector "~2.0.6" - vega-expression "~3.0.0" + vega-expression "~4.0.1" vega-util "~1.16.0" - yargs "~16.0.3" + yargs "~16.2.0" vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: version "4.4.0" @@ -29174,12 +29182,12 @@ vega-time@^2.0.3, vega-time@^2.0.4, vega-time@~2.0.4: d3-time "^2.0.0" vega-util "^1.15.2" -vega-tooltip@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.25.0.tgz#c5dcae1b2bd36e1c2e61e69f6ee7a0d0d27a3026" - integrity sha512-S48d/eP6WfieLmUvFEjd+raHWKKeK/RfTlwLa3zGcBULDHJY2NU2vRfjC1x33G6Y7eKeAfqGpM6ER5Qt1nf8tA== +vega-tooltip@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.25.1.tgz#cb7e438438649eb46896e7bee6f54e25d25b3c09" + integrity sha512-ugGwGi2/p3OpB8N15xieuzP8DyV5DreqMWcmJ9zpWT8GlkyKtef4dGRXnvHeHQ+iJFmWrq4oZJ+kLTrdiECjAg== dependencies: - vega-util "^1.15.2" + vega-util "^1.16.0" vega-transforms@~4.9.3: version "4.9.3" @@ -30186,7 +30194,7 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== -y18n@^5.0.1, y18n@^5.0.5: +y18n@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== @@ -30235,7 +30243,7 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.0.0, yargs-parser@^20.2.2: +yargs-parser@^20.2.2: version "20.2.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.2.tgz#84562c6b1c41ccec2f13d346c7dd83f8d1a0dc70" integrity sha512-XmrpXaTl6noDsf1dKpBuUNCOHqjs0g3jRMXf/ztRxdOmb+er8kE5z5b55Lz3p5u2T8KJ59ENBnASS8/iapVJ5g== @@ -30288,7 +30296,7 @@ yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.3, yargs@^16.2.0: +yargs@^16.0.3, yargs@^16.2.0, yargs@~16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -30333,19 +30341,6 @@ yargs@^7.1.0: y18n "^3.2.1" yargs-parser "5.0.0-security.0" -yargs@~16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" - integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== - dependencies: - cliui "^7.0.0" - escalade "^3.0.2" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.1" - yargs-parser "^20.0.0" - yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"